Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ OPENAI_MODEL=gpt-4o-mini
# 跑在 TEE 上的可驗證推論。金鑰從 https://pc.0g.ai 取得。
# 沒設也能玩,旁白會退到本地模板並照實標示「未接上 0G Compute」。
OG_COMPUTE_API_KEY=sk-your-0g-router-key
OG_COMPUTE_MODEL=deepseek-chat-v3-0324
# 必須挑 verifiability = TeeML 的模型,否則「同一位 agent」的主張不成立。
# router 的 /v1/models 會標三種:TeeML(模型跑在 TEE 裡)、TeeTLS(只有 TLS 在 TEE
# 裡,推論在上游廠商)、沒有這個欄位(完全沒 TEE,Claude / GPT 系列屬於這類)。
# 撰文當下 TeeML 只有:0gm-1.0-35b-a3b、0gm-1.0-35b-a3b-sia、glm-5.3、
# whisper-large-v3、z-image-turbo。
OG_COMPUTE_MODEL=0gm-1.0-35b-a3b
# OG_COMPUTE_BASE_URL=https://router-api.0g.ai/v1

# provider 的 attestation(遠端證明)端點。
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@

| 賽道 | 狀態 | 實測證據 |
|---|---|---|
| **0G Compute** | ✅ 通 | `deepseek-chat-v3-0324 · TEE 就緒`,戰後旁白由 enclave 產生 |
| **0G Compute** | ✅ 通 | `0gm-1.0-35b-a3b`(**TeeML** · TDX · dstack)—— 敵方 agent 與戰後旁白都跑在 enclave 裡 |
| **0G Storage** | ✅ 通 | 檔案 root `0x31dbf57395ca6d1b8f401f944453b846ce0c2a0cedddd97c27c4bc8a84d7cb25`<br>Flow 合約 submit tx `0x9c7377b88930c28412c95dfb452793c500233531264e09fef8537b91358b2218`<br>儲存費 92200934886 neuron,節點回報 `Single file upload completed` |
| **0G Chain** | ✅ 通 | 區塊 `#54068232`、5 個確認,calldata 讀回後**四項全部相符**(摘要 / 勝負 / 回合數 / 記憶核心) |

Expand All @@ -60,14 +60,14 @@
| 用了哪些 0G 技術 | 為什麼要用它 | 在哪一行用到 |
|---|---|---|
| **0G Compute Network**<br>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**<br>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 Compute Network**<br>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#L108-L116`](https://github.com/ConsssLab/web/blob/main/functions/api/og/_shared.js#L108-L116) |
| **0G Storage**<br>真實寫入 | 記憶碎片要「永久保存」就必須真的落地。用官方 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)<br>節點代理(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**<br>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**<br>節點活性探測 | 結果畫面的燈號要照實反映網路狀態,不能寫死成綠燈。 | [`functions/api/og/status.js#L34-L57`](https://github.com/ConsssLab/web/blob/main/functions/api/og/status.js#L34-L57) |
| **0G Storage**<br>節點活性探測 | 結果畫面的燈號要照實反映網路狀態,不能寫死成綠燈。 | [`functions/api/og/status.js#L35-L58`](https://github.com/ConsssLab/web/blob/main/functions/api/og/status.js#L35-L58) |
| **0G Chain**<br>錨定寫入 | 戰報需要一個不可竄改、有時間戳的存在證明。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**<br>鏈上回驗 | **這是整個專案的重點**:按了按鈕、錢包沒報錯,不代表資料真的在鏈上。所以再用 `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**<br>鏈況與網路切換 | 標題頁即時顯示 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**<br>(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) |
| **0G Chain**<br>鏈況與網路切換 | 標題頁即時顯示 Galileo 區塊高度;chainId **從鏈上實際讀回來**校準而不是寫死(0G 換過 chain ID 16601→16602,寫死會讓切鏈整個失敗)。 | [`functions/api/og/status.js#L60-L177`](https://github.com/ConsssLab/web/blob/main/functions/api/og/status.js#L60-L177) · [`public/js/og.js#L167-L220`](https://github.com/ConsssLab/web/blob/main/public/js/og.js#L167-L220) |
| — **敵方 AI agent**<br>(OpenAI,非 0G) | 對戰時每回合都要叫一次,需要低延遲,所以另外走 OpenAI。模型輸出一律當不可信資料重新過濾。 | [`functions/api/agent.js#L168-L276`](https://github.com/ConsssLab/web/blob/main/functions/api/agent.js#L168-L276) · [`public/js/ai.js#L16-L68`](https://github.com/ConsssLab/web/blob/main/public/js/ai.js#L16-L68) |

### 評審可以自己打的端點

Expand Down Expand Up @@ -273,7 +273,7 @@ API 金鑰請選 **Secret**(加密),不要用一般變數。
| 變數 | 賽道 | 必要性 | 說明 |
| --- | --- | --- | --- |
| `OG_COMPUTE_API_KEY` | 賽道一 | **建議設** | 0G Compute Router 金鑰,從 [pc.0g.ai](https://pc.0g.ai) 取得。敵方 agent 與旁白 agent 都吃這把 |
| `OG_COMPUTE_MODEL` | 賽道一 | 選用 | 預設 `deepseek-chat-v3-0324` |
| `OG_COMPUTE_MODEL` | 賽道一 | 選用 | 預設 `0gm-1.0-35b-a3b`。**必須挑 `verifiability = TeeML` 的模型** —— `TeeTLS` 只有傳輸層在 enclave 裡,推論在上游廠商那邊跑,撐不起「同一位 agent」的主張 |
| `OG_COMPUTE_ATTESTATION_URL` | 賽道一 | 想驗到 enclave 等級才需要 | provider 的 attestation 端點。沒設的話「同一位 agent」最多只能驗到 `signed`(同一把金鑰),畫面會照實降級顯示 |
| `AI_PROVIDER` | 敵方 agent | 選用 | `0g`(**預設**)或 `openai`。切回 OpenAI 會讓「同一位 agent」的驗證降級 |
| `OPENAI_API_KEY` | 敵方 agent | 只有切回 OpenAI 才要 | **只存在 Function 端**,不會進前端 bundle |
Expand Down
45 changes: 15 additions & 30 deletions functions/api/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@
*/

import { sealTurn } from './og/tee.js';
import { providerConfig } from './og/_shared.js';

const OPENAI_BASE = 'https://api.openai.com/v1';
const OG_ROUTER_BASE = 'https://router-api.0g.ai/v1';

const MAX_BODY = 16 * 1024;
const MAX_PLAYS = 4;
Expand All @@ -30,35 +29,16 @@ const json = (data, status = 200) =>
headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },
});

/**
* 預設 **0G Compute**,設 AI_PROVIDER=openai 才切回 OpenAI。
/*
* 供應商設定改成從 og/_shared.js 匯入,不要在這裡再寫一份。
*
* 一開始是反過來的(預設 OpenAI,理由是每回合都要叫、要低延遲)。但那讓「TEE」
* 變成空話:跟玩家對打的 agent 根本不在 enclave 裡,就無從證明「整場都是同一位」。
* 要讓那個主張成立,敵方 agent 必須跑在 0G Compute 上並帶回可驗證的簽名。
* 沒設金鑰時仍會退回本地啟發式,並照實標示「未接上模型」。
* 原本這裡有一份幾乎一樣的 providerConfig,包含寫死的模型名。結果是兩個真相來源:
* 改了 _shared.js 完全不會影響這支,而實際打 router 的正是這支。
* 那個寫死的模型名(deepseek-chat-v3-0324)在 router 上根本不存在,於是每一回合
* 都拿到 404 → 靜靜退回本地啟發式 —— 敵方 agent 從來沒有真的跑在 0G 上過,
* 而我們一直以為有。
*/
function providerConfig(env) {
const choice = String(env.AI_PROVIDER || '0g').toLowerCase();
if (choice === '0g' || choice === '0g-compute') {
return {
id: '0g-compute',
label: '0G Compute Network Router',
base: env.OG_COMPUTE_BASE_URL || OG_ROUTER_BASE,
key: env.OG_COMPUTE_API_KEY,
model: env.OG_COMPUTE_MODEL || 'deepseek-chat-v3-0324',
keyName: 'OG_COMPUTE_API_KEY',
};
}
return {
id: 'openai',
label: 'OpenAI API',
base: env.OPENAI_BASE_URL || OPENAI_BASE,
key: env.OPENAI_API_KEY,
model: env.OPENAI_MODEL || 'gpt-4o-mini',
keyName: 'OPENAI_API_KEY',
};
}


const SYSTEM = `你是回合制策略遊戲《鏈州英雄傳 ConSSS Wars》裡的反派 AI agent「遺忘者」。
你在鏈國 0G 進攻對方的「記憶核心」。你要贏,也要有角色感。
Expand Down Expand Up @@ -225,7 +205,12 @@ export async function onRequestPost({ request, env }) {
body: JSON.stringify({
model: cfg.model,
temperature: 0.8,
max_tokens: 320,
max_tokens: 512,
// 0G 自家模型預設開著 thinking,實測 max_tokens 會被推理吃光、content 回空字串
// (reasoning_tokens 35 / finish_reason "length" / content "")。解析不到 JSON
// 就會靜靜退回本地啟發式 —— 正是這個 bug 躲了那麼久的原因。
// reasoning_effort: 'none' 實測有效:reasoning_tokens 0,content 直接是乾淨的 JSON。
reasoning_effort: 'none',
Comment on lines +209 to +213
response_format: { type: 'json_object' },
messages: [
{ role: 'system', content: SYSTEM },
Expand Down
21 changes: 20 additions & 1 deletion functions/api/og/_shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,32 @@ export function providerConfig(env) {
};
}

/**
* 預設模型。**必須是 verifiability = TeeML 的那幾個之一。**
*
* router 的 /v1/models 會標三種狀態,差別直接決定我們的主張成不成立:
*
* TeeML 模型本身跑在 TEE 裡 ← 只有這個撐得起「同一位 agent」
* TeeTLS TEE 只終結 TLS,推論在上游廠商那邊跑(大多數模型是這種)
* (沒有) 完全沒有 TEE —— Claude / GPT 系列都屬於這類
*
* 撰文當下 TeeML 的只有五個:0gm-1.0-35b-a3b、0gm-1.0-35b-a3b-sia、glm-5.3、
* whisper-large-v3、z-image-turbo。選 0gm-1.0-35b-a3b 的理由:
* · 支援 response_format —— 我們需要 JSON 模式,-sia 那個不支援
* · glm-5.3 的 deep thinking 永遠開著且關不掉,逐回合呼叫太慢
* · 便宜($0.00000008 / prompt token)
*
* 換模型前先確認它是 TeeML,否則畫面上的 TEE 主張會變成空話。
*/
const OG_COMPUTE_DEFAULT_MODEL = '0gm-1.0-35b-a3b';

/** 0G Compute Network Router 的設定。跟敵方 agent 用哪家無關,這個永遠指向 0G。 */
export const ogComputeConfig = (env) => ({
id: '0g-compute',
label: '0G Compute Network Router',
base: env.OG_COMPUTE_BASE_URL || OG_ROUTER_BASE,
key: env.OG_COMPUTE_API_KEY,
model: env.OG_COMPUTE_MODEL || 'deepseek-chat-v3-0324',
model: env.OG_COMPUTE_MODEL || OG_COMPUTE_DEFAULT_MODEL,
keyName: 'OG_COMPUTE_API_KEY',
});

Expand Down
24 changes: 19 additions & 5 deletions functions/api/og/same-agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
* 分級的定義見 tee.js 的 verifyChain。
*/

import { json, clampStr, clampInt } from './_shared.js';
import { verifyChain } from './tee.js';
import { json, clampStr, clampInt, ogComputeConfig } from './_shared.js';
import { verifyChain, probeComputeModel } from './tee.js';

const MAX_BODY = 32 * 1024;
const MAX_TURNS = 12;
Expand All @@ -24,11 +24,14 @@ const normalizeTurn = (t) => ({
signer: clampStr(t && t.signer, 200) || null,
measurement: clampStr(t && t.measurement, 200) || null,
proofId: clampStr(t && t.proofId, 120) || null,
// 實測 0G router 沒有回簽名,但每次回應都帶服務它的 provider 鏈上位址
// (標頭 x-provider / body x_0g_trace.provider)。這是目前拿得到的最強身分。
providerAddress: clampStr(t && t.providerAddress, 80) || null,
model: clampStr(t && t.model, 60) || null,
provider: clampStr(t && t.provider, 40) || null,
});

export async function onRequestPost({ request }) {
export async function onRequestPost({ request, env }) {
let payload;
try {
const raw = await request.text();
Expand All @@ -41,7 +44,12 @@ export async function onRequestPost({ request }) {
const chain = Array.isArray(payload && payload.chain)
? payload.chain.slice(0, MAX_TURNS).map(normalizeTurn)
: [];
const attestation = payload && typeof payload.attestation === 'object' ? payload.attestation : null;
// TEE 狀態由伺服器自己去 router 查,不採信前端送來的東西 ——
// 這是要證明給玩家看的結論,材料不能由被證明的那一方提供。
const probe = await probeComputeModel(ogComputeConfig(env));
const attestation = probe.ok
? { modelInTee: probe.modelInTee, verifiability: probe.verifiability, teeType: probe.teeType, teeVerifier: probe.teeVerifier }
: { error: probe.error || null };

const verdict = verifyChain(chain, attestation);
return json({
Expand All @@ -53,8 +61,14 @@ export async function onRequestPost({ request }) {
responseHash: t.responseHash,
signed: Boolean(t.signature),
signer: t.signer,
providerAddress: t.providerAddress,
model: t.model,
sameAsFirst: !t.signer || !chain[0].signer ? null : t.signer === chain[0].signer,
// 「跟第一回合是不是同一個」—— 有簽章公鑰就比公鑰,沒有就比 provider 位址
sameAsFirst: (() => {
const mine = t.signer || t.providerAddress;
const first = chain[0].signer || chain[0].providerAddress;
return !mine || !first ? null : mine.toLowerCase() === first.toLowerCase();
})(),
Comment on lines +66 to +71
})),
});
}
Expand Down
24 changes: 22 additions & 2 deletions functions/api/og/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import { json, rpc, rpcUrlOf, indexerOf, GALILEO, providerConfig, ogComputeConfig } from './_shared.js';
import { probeComputeModel } from './tee.js';

/**
* indexer 的活性探測:能拿到節點清單就算通。
Expand Down Expand Up @@ -74,7 +75,15 @@ export async function onRequestGet({ env }) {
model: compute.model,
endpoint: compute.base,
configured: Boolean(compute.key),
tee: Boolean(compute.key),
// 這幾格由下面的 probeComputeModel 填。設了金鑰不等於叫得動那個模型,
// 更不等於那個模型跑在 TEE 裡 —— 兩件事都要實際查過才敢說。
modelAvailable: null,
verifiability: null,
modelInTee: null,
teeAttested: null,
teeType: null,
tee: false,
error: null,
},

// 敵方 AI agent 另外報,因為它照指定走 OpenAI(也可用 AI_PROVIDER=0g 整支切過來)
Expand Down Expand Up @@ -125,7 +134,7 @@ export async function onRequestGet({ env }) {
},
};

const [chainRes, indexerRes] = await Promise.all([
const [chainRes, indexerRes, computeRes] = await Promise.all([
Promise.all([
rpc(rpcUrl, 'eth_blockNumber'),
rpc(rpcUrl, 'eth_chainId'),
Expand All @@ -135,6 +144,7 @@ export async function onRequestGet({ env }) {
(err) => ({ ok: false, err }),
),
probeIndexer(indexer),
probeComputeModel(compute),
]);

if (chainRes.ok) {
Expand All @@ -148,6 +158,16 @@ export async function onRequestGet({ env }) {
out.chain.error = String(e && e.message ? e.message : e).slice(0, 160);
}

out.compute.modelAvailable = computeRes.ok;
out.compute.verifiability = computeRes.verifiability ?? null;
out.compute.modelInTee = computeRes.ok ? Boolean(computeRes.modelInTee) : null;
out.compute.teeAttested = computeRes.ok ? Boolean(computeRes.teeAttested) : null;
out.compute.teeType = computeRes.teeType ?? null;
// 只有「模型叫得動」而且「模型本身跑在 TEE 裡」才算 TEE 就緒
out.compute.tee = Boolean(computeRes.ok && computeRes.modelInTee);
out.compute.error = computeRes.ok ? null : computeRes.error || null;
if (computeRes.teeModels) out.compute.teeModels = computeRes.teeModels;

out.storage.live = indexerRes.ok;
out.storage.nodeCount = indexerRes.ok ? indexerRes.nodeCount : null;
out.storage.method = indexerRes.ok ? indexerRes.method : null;
Expand Down
Loading