diff --git a/README.md b/README.md
index 4ae11ef..704a1a9 100644
--- a/README.md
+++ b/README.md
@@ -60,14 +60,14 @@
| 用了哪些 0G 技術 | 為什麼要用它 | 在哪一行用到 |
|---|---|---|
| **0G Compute Network**
TEE 可驗證推論 | 戰報要永久存檔,寫它的那個 agent 就不能是黑箱。金鑰在 pc.0g.ai 選 **Private(TEE enclave)** 開的,推論實際跑在 enclave 裡,不是只呼叫一個 OpenAI 相容端點。 | [`functions/api/narrate.js#L55-L134`](https://github.com/ConsssLab/web/blob/main/functions/api/narrate.js#L55-L134) |
-| **0G Compute Network**
Router 設定 | 敵方 agent 與旁白 agent 共用同一份供應商工廠,把 `AI_PROVIDER` 改成 `0g` 就能整支切過去,不用改程式碼。 | [`functions/api/og/_shared.js#L22-L23`](https://github.com/ConsssLab/web/blob/main/functions/api/og/_shared.js#L22-L23) · [`functions/api/og/_shared.js#L82-L90`](https://github.com/ConsssLab/web/blob/main/functions/api/og/_shared.js#L82-L90) |
+| **0G Compute Network**
Router 設定 | 敵方 agent 與旁白 agent 共用同一份供應商工廠,**兩者預設都指向 0G Compute**。要主張「整場都是同一位 agent」,對打的那個 agent 就必須在 enclave 裡 —— 所以預設不是 OpenAI。 | [`functions/api/og/_shared.js#L22-L23`](https://github.com/ConsssLab/web/blob/main/functions/api/og/_shared.js#L22-L23) · [`functions/api/og/_shared.js#L68-L87`](https://github.com/ConsssLab/web/blob/main/functions/api/og/_shared.js#L68-L87) · [`functions/api/og/_shared.js#L89-L97`](https://github.com/ConsssLab/web/blob/main/functions/api/og/_shared.js#L89-L97) |
| **0G Storage**
真實寫入 | 記憶碎片要「永久保存」就必須真的落地。用官方 SDK 走完整協議:切 256-byte chunk 算 merkle root → 對 Flow 合約送 submit(付儲存費)→ 把 segment 傳給 storage node。**全程用玩家自己的錢包簽,伺服器不持有私鑰。** | [`public/js/storage.js#L72-L116`](https://github.com/ConsssLab/web/blob/main/public/js/storage.js#L72-L116)
節點代理(Node):[`proxy/api/index.js`](https://github.com/ConsssLab/web/blob/main/proxy/api/index.js) —— 節點是裸 IP + 非標準埠,Cloudflare Workers 打不到(error 1003 / 521),這一段必須跑在 Node 上 |
| **0G Storage**
indexer 唯讀查詢 | 「上傳沒報錯」不等於存進去了。拿 root hash 回頭問 indexer,確認 storage node 真的收下並 finalized 才敢標成已存檔。唯讀、不需金鑰,評審可自行查證。 | [`functions/api/og/storage.js#L117-L159`](https://github.com/ConsssLab/web/blob/main/functions/api/og/storage.js#L117-L159) |
| **0G Storage**
節點活性探測 | 結果畫面的燈號要照實反映網路狀態,不能寫死成綠燈。 | [`functions/api/og/status.js#L34-L57`](https://github.com/ConsssLab/web/blob/main/functions/api/og/status.js#L34-L57) |
-| **0G Chain**
錨定寫入 | 戰報需要一個不可竄改、有時間戳的存在證明。40 bytes 結構化 calldata(魔術字 `CSSW` + 版本 + 戰績 + SHA-256),用合約建立交易送出,chainscan 上一眼認得出來。 | [`functions/api/og/shard.js#L62-L84`](https://github.com/ConsssLab/web/blob/main/functions/api/og/shard.js#L62-L84) · [`public/js/og.js#L235-L260`](https://github.com/ConsssLab/web/blob/main/public/js/og.js#L235-L260) |
+| **0G Chain**
錨定寫入 | 戰報需要一個不可竄改、有時間戳的存在證明。40 bytes 結構化 calldata(魔術字 `CSSW` + 版本 + 戰績 + SHA-256),用合約建立交易送出,chainscan 上一眼認得出來。 | [`functions/api/og/shard.js#L85-L107`](https://github.com/ConsssLab/web/blob/main/functions/api/og/shard.js#L85-L107) · [`public/js/og.js#L235-L260`](https://github.com/ConsssLab/web/blob/main/public/js/og.js#L235-L260) |
| **0G Chain**
鏈上回驗 | **這是整個專案的重點**:按了按鈕、錢包沒報錯,不代表資料真的在鏈上。所以再用 `eth_getTransactionByHash` 把交易讀回來、反解 calldata、跟本地碎片逐欄比對,四項全對才打勾。 | [`functions/api/og/verify.js#L19-L47`](https://github.com/ConsssLab/web/blob/main/functions/api/og/verify.js#L19-L47) · [`functions/api/og/verify.js#L49-L104`](https://github.com/ConsssLab/web/blob/main/functions/api/og/verify.js#L49-L104) |
| **0G Chain**
鏈況與網路切換 | 標題頁即時顯示 Galileo 區塊高度;chainId **從鏈上實際讀回來**校準而不是寫死(0G 換過 chain ID 16601→16602,寫死會讓切鏈整個失敗)。 | [`functions/api/og/status.js#L59-L157`](https://github.com/ConsssLab/web/blob/main/functions/api/og/status.js#L59-L157) · [`public/js/og.js#L167-L220`](https://github.com/ConsssLab/web/blob/main/public/js/og.js#L167-L220) |
-| — **敵方 AI agent**
(OpenAI,非 0G) | 對戰時每回合都要叫一次,需要低延遲,所以另外走 OpenAI。模型輸出一律當不可信資料重新過濾。 | [`functions/api/agent.js#L173-L256`](https://github.com/ConsssLab/web/blob/main/functions/api/agent.js#L173-L256) · [`public/js/ai.js#L16-L65`](https://github.com/ConsssLab/web/blob/main/public/js/ai.js#L16-L65) |
+| — **敵方 AI agent**
(OpenAI,非 0G) | 對戰時每回合都要叫一次,需要低延遲,所以另外走 OpenAI。模型輸出一律當不可信資料重新過濾。 | [`functions/api/agent.js#L188-L291`](https://github.com/ConsssLab/web/blob/main/functions/api/agent.js#L188-L291) · [`public/js/ai.js#L16-L68`](https://github.com/ConsssLab/web/blob/main/public/js/ai.js#L16-L68) |
### 評審可以自己打的端點
@@ -128,7 +128,8 @@ inline SVG:戰場、卡牌、遺忘者的遮罩、結果畫面都沒有圖檔
[`public/js/rules.js`](public/js/rules.js) 是純規則層(無 DOM、無網路),可以直接 import 跑對戰模擬:
```bash
-npm run sim # 跑 300 場隨機對戰,印出勝負分布與平均回合數
+npm run sim # 跑 300 場隨機對戰,印出勝負分布與平均回合數
+npm run check:links # 檢查這份 README 的行號連結有沒有因為改碼而跑掉
```
> ⚠️ **改任何數值前請先重跑模擬。** `npm run sim` 用隨機出牌,每次會抖動;連跑三輪的
diff --git a/package.json b/package.json
index 2c49b65..ae08ef0 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
"scripts": {
"dev": "wrangler pages dev public --compatibility-date=2024-09-01 --port 8788",
"deploy": "wrangler pages deploy public --project-name conssswars-web --branch main",
- "sim": "node scripts/simulate.mjs"
+ "sim": "node scripts/simulate.mjs",
+ "check:links": "node scripts/check-links.mjs"
},
"devDependencies": {
"wrangler": "^3.78.0"
diff --git a/scripts/check-links.mjs b/scripts/check-links.mjs
new file mode 100644
index 0000000..20d8666
--- /dev/null
+++ b/scripts/check-links.mjs
@@ -0,0 +1,62 @@
+/**
+ * 檢查 README 裡的行號連結有沒有跑掉。
+ *
+ * README 的賣點是「精確到行號」—— 評審會點進去。但只要改到被指到的檔案,行號就位移,
+ * 連結會安靜地指向不相干的程式碼:範圍還在檔案內,所以肉眼看不出壞掉。
+ * 實際發生過兩次,其中一次「賽道二 indexer 唯讀查詢」點下去落在兩個無關的小工具上。
+ *
+ * 這支不驗「行號是不是原本想指的東西」(那需要人判斷),只驗一件機械可查的事:
+ * 起始行要是函式或註解區塊的開頭,結束行要是收尾。位移之後這兩個條件幾乎必然被破壞。
+ *
+ * npm run check:links
+ */
+
+import { readFileSync } from 'node:fs';
+
+const README = 'README.md';
+const LINK = /`([^`]+?\.js)#L(\d+)-L(\d+)`/g;
+
+const START_OK = /^(\/\*\*|\/\*|export\s|async\s|function\s|const\s|class\s)/;
+const END_OK = /^(\}|\}\);|\};|export const .+;)$/;
+
+const readme = readFileSync(README, 'utf8');
+const seen = new Set();
+let bad = 0;
+let total = 0;
+
+for (const [, path, aStr, bStr] of readme.matchAll(LINK)) {
+ const key = `${path}#${aStr}-${bStr}`;
+ if (seen.has(key)) continue;
+ seen.add(key);
+ total += 1;
+
+ let lines;
+ try {
+ lines = readFileSync(path, 'utf8').split('\n');
+ } catch {
+ console.log(`❌ ${key} 檔案不存在`);
+ bad += 1;
+ continue;
+ }
+
+ const a = Number(aStr);
+ const b = Number(bStr);
+ if (b > lines.length) {
+ console.log(`❌ ${key} 檔案只有 ${lines.length} 行`);
+ bad += 1;
+ continue;
+ }
+
+ const head = lines[a - 1].trim();
+ const tail = lines[b - 1].trim();
+ const ok = START_OK.test(head) && END_OK.test(tail);
+ if (!ok) bad += 1;
+ console.log(`${ok ? '✅' : '❌'} ${key}\n 起 ${head.slice(0, 60)}\n 訖 ${tail.slice(0, 60)}`);
+}
+
+console.log(
+ bad === 0
+ ? `\n${total} 個行號連結都落在函式起訖上。`
+ : `\n${bad} / ${total} 個連結跑掉了 —— 被指到的檔案改過,行號要重算。`,
+);
+process.exit(bad === 0 ? 0 : 1);