diff --git a/.github/workflows/deputy-message-board.yml b/.github/workflows/deputy-message-board.yml
index 7294e9ee..2945c391 100644
--- a/.github/workflows/deputy-message-board.yml
+++ b/.github/workflows/deputy-message-board.yml
@@ -2,29 +2,36 @@
# ðº Sovereign: TCS-0002â | Root: SYS-GLW-0001
# ð Copyright: åœäœç»å-2026-A-00037559
# âââââââââââââââââââââââââââââââââââââââââââââââ
-name: "ð¬ éžæžå¯å°Â·çèšæ¿èªåšåå€"
+name: "ð¬ éžæžå¯å°Â·çèšæ¿æŽ»äœAgent v2.0"
on:
+ # äºä»¶è§Šå: Issue/Comment â 峿¶åå€
issues:
types: [opened]
issue_comment:
types: [created]
+ # 宿¶å·¡æ¥: 08:00/23:00 CST â æ«ææææªåå€Issue
+ schedule:
+ - cron: '0 0 * * *' # UTC 00:00 = CST 08:00 æ©çå·¡æ¥
+ - cron: '0 15 * * *' # UTC 15:00 = CST 23:00 æçå·¡æ¥
permissions:
- contents: read
+ contents: write
issues: write
jobs:
+ # ââ äºä»¶æš¡åŒ: Issue/Commentè§Šå峿¶åå€ ââ
deputy-reply:
name: "ðïž å¯å°åå€çèš"
runs-on: ubuntu-latest
if: >
- (github.event_name == 'issues' &&
+ github.event_name != 'schedule' &&
+ ((github.event_name == 'issues' &&
github.event.action == 'opened' &&
contains(join(github.event.issue.labels.*.name, ','), 'deputy-message-board')) ||
(github.event_name == 'issue_comment' &&
contains(join(github.event.issue.labels.*.name, ','), 'deputy-message-board') &&
- github.event.comment.user.login != 'github-actions[bot]')
+ github.event.comment.user.login != 'github-actions[bot]'))
steps:
- uses: actions/checkout@v4
@@ -36,12 +43,12 @@ jobs:
run: |
npm install --omit=dev --ignore-scripts || echo "â ïž äŸèµå®è£
åŒåžžÂ·éšååèœå¯èœåé"
- - name: "ðïž å¯å°å€ççèš"
+ - name: "ðïž å¯å°å€ççèš (äºä»¶æš¡åŒ)"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ZY_LLM_API_KEY: ${{ secrets.ZY_LLM_API_KEY }}
ZY_LLM_BASE_URL: ${{ secrets.ZY_LLM_BASE_URL }}
- ZY_NOTION_TOKEN: ${{ secrets.ZY_NOTION_TOKEN }}
+ DEPUTY_MODE: event
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
@@ -50,3 +57,49 @@ jobs:
EVENT_NAME: ${{ github.event_name }}
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
run: node scripts/deputy-message-board.js
+
+ - name: "ð æäº€å¯å°ç¶æ"
+ if: always()
+ run: |
+ if [ -f data/deputy-status.json ]; then
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add data/deputy-status.json
+ git diff --cached --quiet || git commit -m "ðïž å¯å°ç¶ææŽæ° · äºä»¶æš¡åŒ · $(date -u +%Y-%m-%dT%H:%M:%S)Z"
+ git push || echo "â ïž pushå€±èŽ¥Â·äžæ¬¡å·¡æ¥æ¶åæ¥"
+ fi
+
+ # ââ å·¡æ¥æš¡åŒ: 宿¶æ«ææææªåå€Issue ââ
+ deputy-patrol:
+ name: "ð å¯å°å·¡æ¥æªåå€çèš"
+ runs-on: ubuntu-latest
+ if: github.event_name == 'schedule'
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+
+ - name: "ðŠ å®è£
äŸèµ"
+ run: |
+ npm install --omit=dev --ignore-scripts || echo "â ïž äŸèµå®è£
åŒåžžÂ·éšååèœå¯èœåé"
+
+ - name: "ð å¯å°å·¡æ¥ (å·¡æ¥æš¡åŒ)"
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ ZY_LLM_API_KEY: ${{ secrets.ZY_LLM_API_KEY }}
+ ZY_LLM_BASE_URL: ${{ secrets.ZY_LLM_BASE_URL }}
+ DEPUTY_MODE: patrol
+ run: node scripts/deputy-message-board.js
+
+ - name: "ð æäº€å¯å°ç¶æ"
+ if: always()
+ run: |
+ if [ -f data/deputy-status.json ]; then
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add data/deputy-status.json
+ git diff --cached --quiet || git commit -m "ð å¯å°å·¡æ¥ç¶ææŽæ° · $(date -u +%Y-%m-%dT%H:%M:%S)Z"
+ git push || echo "â ïž pushå€±èŽ¥Â·äžæ¬¡å·¡æ¥æ¶åæ¥"
+ fi
diff --git a/README.md b/README.md
index da261cbb..b70644d9 100644
--- a/README.md
+++ b/README.md
@@ -190,6 +190,19 @@
| HLDPéçšè¯èšåæ¥ | â
å·²æ§è¡ | hldp/data/common/ |
| ä»åºéŠé¡µæŽæ° | â
å·²æ§è¡ | README.md çŸå°ä»ªè¡šç |
| å·¥äœæµå£«å
µå·¡æ£ | â
18/18 åšå² | 猺倱çéå°æç¡®è®€ |
+| çèšæ¿å·¡æ¥ | â
v2.0掻å | LLM倿š¡åé级 · 宿¶å·¡æ¥ · èªæ |
+
+### ðïž éžæžå¯å°Agentç¶æ (v2.0掻äœAgent)
+
+| é¡¹ç® | ç¶æ |
+|------|------|
+| Agentçæ¬ | v2.0 · 掻äœAgent · äžå±èªæ |
+| è¿è¡æš¡åŒ | äºä»¶æš¡åŒ(峿¶åå€) + å·¡æ¥æš¡åŒ(08:00/23:00) |
+| LLMèœå | 倿š¡åèªåšé级: deepseek â qwen â moonshot â zhipu |
+| èªæç¥ | data/deputy-status.json · æ¯æ¬¡è¿è¡æŽæ° |
+| èªä¿®å€ | æ¯æš¡åéè¯2次 · éå¢çåŸ
|
+| åçº§éæ¥ | è¿ç»3次倱莥 â å建Issue â é®ä»¶éç¥å°æ |
+| ð® çèšå
¥å£ | [ç»å¯å°çèš](https://github.com/qinfendebingshuo/guanghulab/issues/new?labels=deputy-message-board&title=ç»éžæžå¯å°çèš) |
> *æ€ä»ªè¡šçç±éžæžå¯å°(ZY-DEPUTY-001)æ¯æ¥å€éæ¶èªåšæŽæ° · 08:00 / 23:00*
diff --git a/data/deputy-status.json b/data/deputy-status.json
new file mode 100644
index 00000000..f3e1ffb0
--- /dev/null
+++ b/data/deputy-status.json
@@ -0,0 +1,16 @@
+{
+ "version": "2.0",
+ "last_run": null,
+ "last_success": null,
+ "llm_available": false,
+ "llm_model_used": null,
+ "consecutive_llm_failures": 0,
+ "issues_processed": 0,
+ "issues_replied": 0,
+ "patrol_runs": 0,
+ "event_runs": 0,
+ "errors": [],
+ "escalations": [],
+ "created_at": "2026-04-07T17:13:39Z",
+ "_comment": "éžæžå¯å°v2.0掻äœAgentç¶ææä»¶ · æ¯æ¬¡è¿è¡èªåšæŽæ°"
+}
diff --git a/docs/index.html b/docs/index.html
index 7bc28025..5d38aa17 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -716,6 +716,36 @@ label{display:block;font-size:12px;color:var(--dim);margin-bottom:4px;font-weigh
.app-card-tags{display:flex;gap:5px;flex-wrap:wrap}
.app-card-arrow{font-size:18px;color:var(--accent);opacity:.5;flex-shrink:0;transition:all .3s}
.app-card:hover .app-card-arrow{opacity:1;transform:translateX(4px)}
+
+/* âââ PAGE: Bandwidth Sharing (垊宜å
±äº«) âââ */
+#pg-bandwidth .bw-hero{text-align:center;padding:28px 0 20px}
+#pg-bandwidth .bw-hero h2{font-size:1.4em;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
+#pg-bandwidth .bw-hero p{color:var(--dim);font-size:.9em;margin-top:6px}
+#pg-bandwidth .bw-stats{display:flex;justify-content:space-around;margin:16px 0;gap:12px}
+#pg-bandwidth .bw-stat{text-align:center;flex:1}
+#pg-bandwidth .bw-stat .num{font-size:1.5em;font-weight:700;color:var(--accent);font-family:var(--m)}
+#pg-bandwidth .bw-stat .label{font-size:.75em;color:var(--dim);margin-top:4px}
+#pg-bandwidth .bw-card{background:var(--bg1);border-radius:var(--r);padding:20px;margin:12px 0;border:1px solid var(--border)}
+#pg-bandwidth .bw-card h3{font-size:1em;color:var(--accent);margin-bottom:14px}
+#pg-bandwidth .bw-input{width:100%;padding:12px 14px;background:var(--bg2);border:2px solid var(--border);border-radius:var(--rs);color:var(--text);font-size:1em;outline:none;transition:border-color .3s;font-family:var(--f)}
+#pg-bandwidth .bw-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(138,180,248,.15)}
+#pg-bandwidth .bw-input::placeholder{color:var(--dim);font-size:.85em}
+#pg-bandwidth .bw-code-input{text-align:center;letter-spacing:8px;font-size:1.3em;font-family:var(--m)}
+#pg-bandwidth .bw-code-input::placeholder{letter-spacing:0;font-size:.6em}
+#pg-bandwidth .bw-btn{width:100%;padding:14px;background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;border:none;border-radius:var(--rs);font-size:1em;font-weight:600;cursor:pointer;transition:opacity .3s,transform .15s;margin-top:8px;font-family:var(--f)}
+#pg-bandwidth .bw-btn:hover{opacity:.9}
+#pg-bandwidth .bw-btn:active{transform:scale(.98)}
+#pg-bandwidth .bw-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
+#pg-bandwidth .bw-btn-outline{background:transparent;border:1px solid var(--accent);color:var(--accent)}
+#pg-bandwidth .bw-btn-outline:hover{background:rgba(138,180,248,.1)}
+#pg-bandwidth .bw-result{text-align:center;padding:14px;border-radius:var(--rs);margin-top:12px;display:none;font-size:.9em;line-height:1.6}
+#pg-bandwidth .bw-result.ok{display:block;background:rgba(52,211,153,.1);color:var(--ok);border:1px solid rgba(52,211,153,.2)}
+#pg-bandwidth .bw-result.err{display:block;background:rgba(248,113,113,.1);color:var(--err);border:1px solid rgba(248,113,113,.2)}
+#pg-bandwidth .bw-info{background:var(--bg2);border-radius:var(--rs);padding:14px 16px;margin:10px 0;font-size:.85em;line-height:1.8;color:var(--dim);border-left:3px solid var(--accent)}
+#pg-bandwidth .bw-consent{display:flex;align-items:flex-start;gap:10px;margin:14px 0;padding:12px 14px;background:var(--bg2);border-radius:var(--rs);border:1px solid var(--border);cursor:pointer;transition:border-color .3s}
+#pg-bandwidth .bw-consent:hover{border-color:var(--accent)}
+#pg-bandwidth .bw-consent input{margin-top:3px;width:18px;height:18px;accent-color:var(--accent);cursor:pointer;flex-shrink:0}
+#pg-bandwidth .bw-consent span{color:var(--dim);font-size:.85em;line-height:1.7}
@@ -894,6 +924,23 @@ label{display:block;font-size:12px;color:var(--dim);margin-bottom:4px;font-weigh
+
+
+
ð çæ¬åå²
@@ -1131,6 +1178,63 @@ label{display:block;font-size:12px;color:var(--dim);margin-bottom:4px;font-weigh
+
+
+
+
+
+
+
ð 垊宜å
񄧮
+
+
+
+
+
ð 垊宜å
±äº«å éæ±
+
å
±äº«äœ ç垊宜 · å éææäºº
+
+
+
+
+
+
ð§ ç¬¬äžæ¥ · åééªè¯ç
+
+
+
+
+
+
+
+
+
ð ç¬¬äºæ¥ · èŸå
¥éªè¯ç
+
+
+
+
+
+ æèªæ¿å
±äº«å€äœåžŠå®œå å
¥å éæ± ãç³»ç»äŒå
ä¿éæçäžå±ééïŒä»
å°æçšäžäžçèµæºå
±äº«ç»å
¶ä»æåãæå¯ä»¥éæ¶éåºã
+
+
+
+
+
+
+ ð¡ 垊宜å
±äº«è¯Žæ
+ â
æäŸäºéªè¯ç ççšæ· â äŒå
ä¿éäœ çäžå±éé
+ â
äœ çšäžäžçèµæºæäŒè¢«å
±äº« â äžæ¢äœ ç垊宜
+ â
éæ¶å¯éåº â äœ æå®å
šçæ§å¶æ
+ â ïž éªè¯ç 15åéå
ææ Â· äžæ¬¡æ§äœ¿çš
+
+
+
+
© 2026 å
æ¹è¯èšäžç · åœäœç»å-2026-A-00037559
+
+
+
+
@@ -3446,6 +3550,132 @@ offline = function(msg){
});
})();
+})();
+
+/* ââââââââââââââââââââââââââââââââââââââââ
+ BANDWIDTH SHARING AGENT (垊宜å
±äº«éªè¯)
+ GitHub Pages å端 â å端APIæ¡¥æ¥
+ ââââââââââââââââââââââââââââââââââââââââ */
+var BW_API_BASE = 'https://guanghulab.online/api/proxy-v3';
+
+function bwSendCode(){
+ var email = document.getElementById('bwEmail').value.trim().toLowerCase();
+ var btn = document.getElementById('bwSendCode');
+ var result = document.getElementById('bwSendResult');
+
+ if(!email || email.indexOf('@') === -1){
+ result.className = 'bw-result err';
+ result.textContent = '请èŸå
¥ææçé®ç®±å°å';
+ return;
+ }
+
+ btn.disabled = true;
+ btn.textContent = 'åéäž...';
+ result.className = 'bw-result';
+ result.style.display = 'none';
+
+ fetch(BW_API_BASE + '/bandwidth-send-code', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({email: email})
+ }).then(function(r){ return r.json(); }).then(function(data){
+ btn.disabled = false;
+ btn.textContent = 'åééªè¯ç ';
+ result.className = 'bw-result ' + (data.success ? 'ok' : 'err');
+ result.textContent = data.message || (data.success ? 'éªè¯ç å·²åé' : 'åé倱莥');
+
+ if(data.success){
+ // Cooldown 60s
+ var cd = 60;
+ btn.disabled = true;
+ var timer = setInterval(function(){
+ cd--;
+ btn.textContent = cd + 's åå¯éå';
+ if(cd <= 0){
+ clearInterval(timer);
+ btn.disabled = false;
+ btn.textContent = 'åééªè¯ç ';
+ }
+ }, 1000);
+ }
+ }).catch(function(err){
+ btn.disabled = false;
+ btn.textContent = 'åééªè¯ç ';
+ result.className = 'bw-result err';
+ result.textContent = 'çœç»é误ïŒè¯·æ£æ¥çœç»åéè¯';
+ });
+}
+
+function bwVerifyCode(){
+ var email = document.getElementById('bwEmail').value.trim().toLowerCase();
+ var code = document.getElementById('bwCode').value.trim();
+ var consent = document.getElementById('bwConsent').checked;
+ var btn = document.getElementById('bwVerify');
+ var result = document.getElementById('bwVerifyResult');
+
+ if(!email || email.indexOf('@') === -1){
+ result.className = 'bw-result err';
+ result.textContent = '请å
åšäžæ¹èŸå
¥é®ç®±';
+ return;
+ }
+ if(!code || code.length !== 6){
+ result.className = 'bw-result err';
+ result.textContent = '请èŸå
¥6äœéªè¯ç ';
+ return;
+ }
+ if(!consent){
+ result.className = 'bw-result err';
+ result.textContent = '请å
åŸéåæåžŠå®œå
±äº«åè®®';
+ return;
+ }
+
+ btn.disabled = true;
+ btn.textContent = 'éªè¯äž...';
+ result.className = 'bw-result';
+ result.style.display = 'none';
+
+ fetch(BW_API_BASE + '/bandwidth-verify-code', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({email: email, code: code})
+ }).then(function(r){ return r.json(); }).then(function(data){
+ btn.disabled = false;
+ btn.textContent = 'æäº€éªè¯ç ';
+ result.className = 'bw-result ' + (data.success ? 'ok' : 'err');
+ result.textContent = data.message || (data.success ? 'æææåïŒ' : 'éªè¯å€±èŽ¥');
+ if(data.success) bwLoadPoolStatus();
+ }).catch(function(){
+ btn.disabled = false;
+ btn.textContent = 'æäº€éªè¯ç ';
+ result.className = 'bw-result err';
+ result.textContent = 'çœç»é误ïŒè¯·æ£æ¥çœç»åéè¯';
+ });
+}
+
+function bwLoadPoolStatus(){
+ fetch(BW_API_BASE + '/bandwidth-pool-status').then(function(r){
+ return r.json();
+ }).then(function(data){
+ var el1 = document.getElementById('bwContributors');
+ var el2 = document.getElementById('bwActive');
+ var el3 = document.getElementById('bwContributed');
+ if(el1) el1.textContent = data.total_contributors || 0;
+ if(el2) el2.textContent = data.active_contributors || 0;
+ if(el3) el3.textContent = data.total_contributed_gb || 0;
+ }).catch(function(){
+ /* Pool status unavailable - keep defaults */
+ });
+}
+
+// Auto-load pool status when bandwidth page is shown
+var _origGo = window.go;
+if(typeof _origGo === 'function'){
+ window.go = function(target){
+ _origGo(target);
+ if(target === 'bandwidth') bwLoadPoolStatus();
+ };
+}
+
})();
diff --git a/scripts/deputy-message-board.js b/scripts/deputy-message-board.js
index c35baf27..ef05b81c 100644
--- a/scripts/deputy-message-board.js
+++ b/scripts/deputy-message-board.js
@@ -4,12 +4,26 @@
* ð Copyright: åœäœç»å-2026-A-00037559
* âââââââââââââââââââââââââââââââââââââââââââââââ
*
- * éžæžå¯å°Â·çèšæ¿èªåšåå€åŒæ
- * è莣: æ¥æ¶äººç±»çèš â æ¥è¯¢æ°æ®åº â è°çšLLM â åå€
+ * éžæžå¯å°Â·çèšæ¿æŽ»äœAgentåŒæ v2.0
+ *
+ * äžå±èªææºå¶:
+ * L1 èªæç¥ â è®°åœè¿è¡ç¶æïŒå¯¹æ¯é¢æ
+ * L2 èªä¿®å€ â LLM倿š¡åèªåšé级 (deepseekâqwenâmoonshotâzhipu)
+ * L3 åçº§éæ¥ â è¿ç»å€±èŽ¥ â å建Issue â åé®ä»¶ç»å°æ
+ *
+ * è¿è¡æš¡åŒ:
+ * - eventæš¡åŒ: Issue/Commentè§Šå â 峿¶åå€
+ * - patrolæš¡åŒ: 宿¶08:00/23:00 â æ«ææææªåå€Issue
*/
const fs = require('fs');
const path = require('path');
+const https = require('https');
+const http = require('http');
+
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+// ç¯å¢åé
+// âââââââââââââââââââââââââââââââââââââââââââââââ
const {
GITHUB_TOKEN,
@@ -21,13 +35,135 @@ const {
COMMENT_BODY,
COMMENT_AUTHOR,
EVENT_NAME,
- ISSUE_AUTHOR
+ ISSUE_AUTHOR,
+ DEPUTY_MODE // 'patrol' | 'event' (default)
} = process.env;
-// Load system context from brain files
+const REPO = process.env.GITHUB_REPOSITORY || 'qinfendebingshuo/guanghulab';
+const STATUS_FILE = path.join(__dirname, '..', 'data', 'deputy-status.json');
+
+// ââ å¯è°åžžé ââ
+const RETRY_BASE_DELAY_MS = 2000; // LLMéè¯åºç¡å»¶è¿
+const MAX_ERRORS_KEPT = 20; // ä¿çæè¿Næ¡é误记åœ
+const MAX_ESCALATIONS_KEPT = 10; // ä¿çæè¿Næ¡å级记åœ
+const MAX_LLM_FAILURES_BEFORE_ESCALATION = 3; // è¿ç»N次å
šæš¡å倱莥åå级
+
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+// LLM倿š¡åèªåšé级路ç±åš (å
åµç)
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+
+const MODEL_PRIORITY = [
+ 'deepseek-chat',
+ 'qwen-turbo',
+ 'moonshot-v1-8k',
+ 'glm-4-flash'
+];
+
+function callSingleModel(baseUrl, apiKey, model, systemPrompt, userMessage, maxTokens, timeout) {
+ return new Promise((resolve, reject) => {
+ let urlObj;
+ try {
+ urlObj = new URL(baseUrl);
+ } catch {
+ urlObj = new URL(`https://${baseUrl}`);
+ }
+
+ const postData = JSON.stringify({
+ model,
+ messages: [
+ { role: 'system', content: systemPrompt },
+ { role: 'user', content: userMessage }
+ ],
+ max_tokens: maxTokens,
+ temperature: 0.7
+ });
+
+ let apiPath = urlObj.pathname;
+ if (!apiPath || apiPath === '/') {
+ apiPath = '/v1/chat/completions';
+ } else if (!apiPath.includes('/chat/completions')) {
+ apiPath = apiPath.replace(/\/+$/, '') + '/chat/completions';
+ }
+
+ const options = {
+ hostname: urlObj.hostname,
+ port: urlObj.port || (urlObj.protocol === 'http:' ? 80 : 443),
+ path: apiPath,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${apiKey}`,
+ 'Content-Length': Buffer.byteLength(postData)
+ },
+ timeout
+ };
+
+ const httpModule = urlObj.protocol === 'http:' ? http : https;
+ const req = httpModule.request(options, (res) => {
+ let data = '';
+ res.on('data', (d) => { data += d; });
+ res.on('end', () => {
+ try {
+ if (res.statusCode >= 400) {
+ reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
+ return;
+ }
+ const json = JSON.parse(data);
+ const content = json.choices?.[0]?.message?.content;
+ if (!content) {
+ reject(new Error('ååºäžæ contentåæ®µ'));
+ return;
+ }
+ resolve({ content, model: json.model || model });
+ } catch (e) {
+ reject(new Error(`JSONè§£æå€±èŽ¥: ${e.message}`));
+ }
+ });
+ });
+
+ req.on('error', (err) => reject(err));
+ req.on('timeout', () => { req.destroy(); reject(new Error(`è¶
æ¶(${timeout}ms)`)); });
+ req.write(postData);
+ req.end();
+ });
+}
+
+async function callLLMWithFallback(systemPrompt, userMessage) {
+ const apiKey = ZY_LLM_API_KEY;
+ const baseUrl = ZY_LLM_BASE_URL || 'https://api.deepseek.com';
+
+ if (!apiKey) {
+ console.log('[å¯å°] â ïž LLMå¯é¥æªé
眮 (ZY_LLM_API_KEY)');
+ return null;
+ }
+
+ const errors = [];
+ for (const model of MODEL_PRIORITY) {
+ for (let attempt = 1; attempt <= 2; attempt++) {
+ try {
+ console.log(`[å¯å°] å°è¯æš¡å ${model} (第${attempt}次)...`);
+ const result = await callSingleModel(baseUrl, apiKey, model, systemPrompt, userMessage, 2000, 30000);
+ console.log(`[å¯å°] â
æš¡å ${result.model} è°çšæå`);
+ return result;
+ } catch (err) {
+ const errMsg = `${model}(attempt ${attempt}): ${err.message}`;
+ console.log(`[å¯å°] â ïž ${errMsg}`);
+ errors.push(errMsg);
+ if (attempt < 2) await new Promise(r => setTimeout(r, RETRY_BASE_DELAY_MS * attempt));
+ }
+ }
+ }
+
+ console.error(`[å¯å°] â æææš¡å(${MODEL_PRIORITY.length}䞪)åäžå¯çš`);
+ return null;
+}
+
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+// ç³»ç»äžäžæ
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+
function loadSystemContext() {
const context = {};
-
const files = [
{ key: 'fast_wake', path: 'brain/fast-wake.json' },
{ key: 'deputy_config', path: 'brain/deputy-general-config.json' },
@@ -35,24 +171,18 @@ function loadSystemContext() {
{ key: 'sync_progress', path: 'hldp/data/common/sync-progress.json' },
{ key: 'vocabulary', path: 'hldp/data/ontology/ONT-VOCABULARY.json' }
];
-
for (const f of files) {
try {
const fullPath = path.join(__dirname, '..', f.path);
context[f.key] = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
- } catch {
- context[f.key] = null;
- }
+ } catch { context[f.key] = null; }
}
-
return context;
}
-// Build a concise system summary for LLM context
function buildSystemSummary(ctx) {
const fw = ctx.fast_wake;
const sp = ctx.sync_progress;
-
return `
äœ æ¯éžæžå¯å°(ZY-DEPUTY-001)ïŒéžæžå°å(ICE-GL-ZY001)çèªåšåæºèœè¿ç»Žä»£çã
äœ èŽèŽ£åšéžæžäŒç æ¶ç®¡ç代ç ä»åº(å
æ¹ç¯å¡ · HoloLake Lighthouse)ã
@@ -62,9 +192,8 @@ function buildSystemSummary(ctx) {
- HLDPè¯èšçæ¬: ${sp?.payload?.github_side_status?.hldp_version || 'v3.0'}
- è¯æ±æ°: ${sp?.payload?.github_side_status?.vocabulary_count || 22}
- Schemaæ°: ${sp?.payload?.github_side_status?.schema_count || 6}
-- å¿«ç
§æ°: ${sp?.payload?.github_side_status?.snapshots || 5}
- éçšåè®®çæ¬: ${sp?.payload?.common_protocol_status?.version || '1.0'}
-- Notionæ¡¥æ¥: 4æ¡ç®¡éå·²æ¢å€(SYSLOG+åæŽ+README+å
¬åæ¿+HLDP忥)
+- Notionæ¡¥æ¥: 6æ¡ç®¡éå·²æ¢å€
åå€è§å:
1. 䜿çšäžæåå€ïŒè¯æ°äžäžäœå奜
@@ -77,74 +206,11 @@ function buildSystemSummary(ctx) {
`.trim();
}
-async function callLLM(systemPrompt, userMessage) {
- const apiKey = ZY_LLM_API_KEY;
- const baseUrl = ZY_LLM_BASE_URL || 'https://api.deepseek.com';
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+// æ°æ®åºæ¥è¯¢
+// âââââââââââââââââââââââââââââââââââââââââââââââ
- if (!apiKey) {
- return 'â ïž å¯å°åœåæ æ³è°çšæ·±åºŠæšçèœåïŒLLMå¯é¥æªé
眮ïŒãæå°åºäºä»åºæ°æ®åºäžå·²æçä¿¡æ¯äžºæšåå€ã';
- }
-
- try {
- const axios = require('axios');
- const response = await axios.post(`${baseUrl}/v1/chat/completions`, {
- model: 'deepseek-chat',
- messages: [
- { role: 'system', content: systemPrompt },
- { role: 'user', content: userMessage }
- ],
- max_tokens: 2000,
- temperature: 0.7
- }, {
- headers: {
- 'Authorization': `Bearer ${apiKey}`,
- 'Content-Type': 'application/json'
- },
- timeout: 30000
- });
-
- return response.data.choices[0].message.content;
- } catch (err) {
- console.log(`[DEPUTY-BOARD] â ïž LLMè°çšå€±èŽ¥: ${err.message}`);
- return null;
- }
-}
-
-async function postComment(issueNumber, body) {
- const axios = require('axios');
- const repo = process.env.GITHUB_REPOSITORY || 'qinfendebingshuo/guanghulab';
-
- await axios.post(
- `https://api.github.com/repos/${repo}/issues/${issueNumber}/comments`,
- { body },
- {
- headers: {
- 'Authorization': `token ${GITHUB_TOKEN}`,
- 'Accept': 'application/vnd.github.v3+json'
- }
- }
- );
-}
-
-async function main() {
- console.log('[DEPUTY-BOARD] ðïž éžæžå¯å°çèšæ¿å€çå¯åš...');
-
- const isNewIssue = EVENT_NAME === 'issues';
- const question = isNewIssue ? (ISSUE_BODY || ISSUE_TITLE) : COMMENT_BODY;
- const author = isNewIssue ? ISSUE_AUTHOR : COMMENT_AUTHOR;
-
- if (!question || question.trim().length === 0) {
- console.log('[DEPUTY-BOARD] â ïž çèšå
容䞺空·跳è¿');
- return;
- }
-
- console.log(`[DEPUTY-BOARD] ðš æ¶å°çèš Â· æ¥èª: ${author}`);
-
- // Load system context
- const ctx = loadSystemContext();
- const systemSummary = buildSystemSummary(ctx);
-
- // Try database lookup first
+function lookupDatabase(question, ctx) {
let dbAnswer = null;
const lowerQ = question.toLowerCase();
@@ -173,45 +239,302 @@ async function main() {
}
}
- // Call LLM for deeper analysis
- const userMsg = `æ¥èª ${author} ççèš:\n\n${question}\n\n${dbAnswer ? 'ä»¥äžæ¯ä»ç³»ç»æ°æ®åºäžæ¥å°ççžå
³ä¿¡æ¯:\n' + dbAnswer : 'æ°æ®åºäžæªæŸå°çŽæ¥çžå
³ä¿¡æ¯ã'}`;
+ return dbAnswer;
+}
- let llmResponse = await callLLM(systemSummary, userMsg);
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+// GitHub API
+// âââââââââââââââââââââââââââââââââââââââââââââââ
- // Build final reply
+function githubAPI(method, endpoint, body) {
+ return new Promise((resolve, reject) => {
+ const postData = body ? JSON.stringify(body) : '';
+ const options = {
+ hostname: 'api.github.com',
+ path: endpoint,
+ method,
+ headers: {
+ 'Authorization': `token ${GITHUB_TOKEN}`,
+ 'Accept': 'application/vnd.github.v3+json',
+ 'User-Agent': 'ZY-Deputy-Agent/2.0',
+ ...(body ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData) } : {})
+ },
+ timeout: 15000
+ };
+ const req = https.request(options, (res) => {
+ let data = '';
+ res.on('data', (d) => { data += d; });
+ res.on('end', () => {
+ try { resolve({ status: res.statusCode, data: data ? JSON.parse(data) : null }); }
+ catch { resolve({ status: res.statusCode, data: null }); }
+ });
+ });
+ req.on('error', (err) => reject(err));
+ req.on('timeout', () => { req.destroy(); reject(new Error('GitHub API timeout')); });
+ if (body) req.write(postData);
+ req.end();
+ });
+}
+
+async function postComment(issueNumber, body) {
+ return githubAPI('POST', `/repos/${REPO}/issues/${issueNumber}/comments`, { body });
+}
+
+async function listDeputyIssues() {
+ const res = await githubAPI('GET', `/repos/${REPO}/issues?labels=deputy-message-board&state=open&per_page=30`);
+ return res.data || [];
+}
+
+async function getIssueComments(issueNumber) {
+ const res = await githubAPI('GET', `/repos/${REPO}/issues/${issueNumber}/comments?per_page=50`);
+ return res.data || [];
+}
+
+async function createEscalationIssue(title, body) {
+ return githubAPI('POST', `/repos/${REPO}/issues`, { title, body, labels: ['deputy-escalation'] });
+}
+
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+// L1: èªæç¥
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+
+function readStatus() {
+ try { return JSON.parse(fs.readFileSync(STATUS_FILE, 'utf8')); }
+ catch {
+ return {
+ version: '2.0',
+ last_run: null, last_success: null,
+ llm_available: false, llm_model_used: null,
+ consecutive_llm_failures: 0,
+ issues_processed: 0, issues_replied: 0,
+ patrol_runs: 0, event_runs: 0,
+ errors: [], escalations: [],
+ created_at: new Date().toISOString()
+ };
+ }
+}
+
+function saveStatus(status) {
+ try {
+ fs.mkdirSync(path.dirname(STATUS_FILE), { recursive: true });
+ fs.writeFileSync(STATUS_FILE, JSON.stringify(status, null, 2));
+ } catch (err) { console.error(`[å¯å°] â ïž ç¶æåå
¥å€±èŽ¥: ${err.message}`); }
+}
+
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+// åå€æå»º
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+
+function buildReply(author, dbAnswer, llmResponse, llmModel) {
const receiveTime = new Date().toISOString().slice(0, 19).replace('T', ' ');
let reply = `## ðïž éžæžå¯å°åå€\n\n`;
reply += `> ðš æ¶å° **${author}** ççèš Â· ${receiveTime} UTC\n\n`;
- if (dbAnswer) {
- reply += `### ð æ°æ®åºæ¥è¯¢ç»æ\n\n${dbAnswer}\n\n`;
- }
+ if (dbAnswer) reply += `### ð æ°æ®åºæ¥è¯¢ç»æ\n\n${dbAnswer}\n\n`;
if (llmResponse) {
reply += `### ð¡ å¯å°åæ\n\n${llmResponse}\n\n`;
+ if (llmModel) reply += `> ð€ æšçåŒæ: ${llmModel}\n\n`;
} else if (!dbAnswer) {
reply += `æè°¢æšççèšãå¯å°å·²è®°åœæšçé®é¢ïŒå°åšéžæžå°åäžæ¬¡å€éæ¶äžå¹¶æ±æ¥ã\n\n`;
reply += `åŠæçŽ§æ¥äºé¡¹ïŒè¯·åšçèšäžæ 泚 **[玧æ¥]** å
³é®è¯ã\n\n`;
}
- // å€éæ¶éŽæç€º
+ reply += `---\n\n`;
reply += `### â° éžæžå¯å°å€éæ¶éŽ\n\n`;
reply += `| å€éæ¶æ®µ | å京æ¶éŽ | 诎æ |\n`;
reply += `|----------|----------|------|\n`;
reply += `| ð
æ©çå€é | **æ¯æ¥ 08:00** | å€çå€éŽç§¯çŽ¯æ¶æ¯ · æŽæ°ä»ªè¡šç |\n`;
reply += `| ð æçå€é | **æ¯æ¥ 23:00** | å€ççœå€©æ¶æ¯ · å
šå±å·¡æ£ · æŽæ°ä»åºéŠé¡µ |\n\n`;
- reply += `> ð¡ å¯å°å°åšäžæ¬¡å€éæ¶èªåšå€çæšççèšå¹¶åå€ã\n`;
- reply += `> åŠé玧æ¥å€çïŒè¯·åšçèšäžæ 泚 **[玧æ¥]** å
³é®è¯ã\n\n`;
-
reply += `---\n\n`;
reply += `*ââ éžæžå¯å° · ZY-DEPUTY-001 · å
æ¹ç¯å¡å®æ€è
*\n`;
reply += `*ð åœäœç»å-2026-A-00037559 · TCSéæè¯èšæ žç³»ç»çŒçšè¯èš*`;
+ return reply;
+}
- await postComment(ISSUE_NUMBER, reply);
- console.log(`[DEPUTY-BOARD] â
åå€å·²åé · Issue #${ISSUE_NUMBER}`);
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+// å€çåæ¡çèš
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+
+async function processMessage(issueNumber, question, author, ctx, systemSummary, status) {
+ if (!question || question.trim().length === 0) {
+ console.log(`[å¯å°] â ïž Issue #${issueNumber} çèšå
容䞺空·跳è¿`);
+ return false;
+ }
+
+ console.log(`[å¯å°] ðš å€ççèš Â· Issue #${issueNumber} · æ¥èª: ${author}`);
+
+ const dbAnswer = lookupDatabase(question, ctx);
+
+ const userMsg = `æ¥èª ${author} ççèš:\n\n${question}\n\n${dbAnswer ? 'ä»¥äžæ¯ä»ç³»ç»æ°æ®åºäžæ¥å°ççžå
³ä¿¡æ¯:\n' + dbAnswer : 'æ°æ®åºäžæªæŸå°çŽæ¥çžå
³ä¿¡æ¯ã'}`;
+ const llmResult = await callLLMWithFallback(systemSummary, userMsg);
+
+ if (llmResult) {
+ status.llm_available = true;
+ status.llm_model_used = llmResult.model;
+ status.consecutive_llm_failures = 0;
+ } else {
+ status.consecutive_llm_failures++;
+ }
+
+ const reply = buildReply(author, dbAnswer, llmResult?.content, llmResult?.model);
+ await postComment(issueNumber, reply);
+
+ status.issues_replied++;
+ console.log(`[å¯å°] â
åå€å·²åé · Issue #${issueNumber}`);
+ return true;
+}
+
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+// L3: åçº§éæ¥
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+
+async function checkAndEscalate(status) {
+ if (status.consecutive_llm_failures >= MAX_LLM_FAILURES_BEFORE_ESCALATION) {
+ const title = `ðš [å¯å°å级] LLMè¿ç»${status.consecutive_llm_failures}次å
šæš¡åè°çšå€±èŽ¥`;
+ const body = `## ᅵᅵ éžæžå¯å°åçº§éæ¥\n\n` +
+ `**æ¶éŽ**: ${new Date().toISOString()}\n` +
+ `**é®é¢**: LLM APIè¿ç» ${status.consecutive_llm_failures} 次è°çšå€±èŽ¥ïŒæææš¡ååäžå¯çšïŒ\n` +
+ `**圱å**: å¯å°æ æ³è¿è¡æ·±åºŠæšçåå€ïŒä»
èœæäŸæ°æ®åºæ¥è¯¢ç»æ\n\n` +
+ `### å·²å°è¯çæš¡å\n\n` +
+ MODEL_PRIORITY.map(m => `- ${m}`).join('\n') + '\n\n' +
+ `### æè¿é误\n\n` +
+ (status.errors.slice(-5).map(e => `- ${e}`).join('\n') || 'æ ') + '\n\n' +
+ `### éèŠå€ç\n\n` +
+ `1. æ£æ¥ \`ZY_LLM_API_KEY\` å \`ZY_LLM_BASE_URL\` æ¯åŠæ£ç¡®é
眮\n` +
+ `2. 确讀APIé
颿¯åŠçšå°œ\n` +
+ `3. 确讀çœç»è¿æ¥æ¯åŠæ£åžž\n\n` +
+ `---\n*ââ éžæžå¯å° · ZY-DEPUTY-001 · èªåšåçº§éæ¥*`;
+ try {
+ await createEscalationIssue(title, body);
+ status.escalations.push({ time: new Date().toISOString(), type: 'llm_failure', detail: `è¿ç»${status.consecutive_llm_failures}次倱莥` });
+ if (status.escalations.length > MAX_ESCALATIONS_KEPT) status.escalations = status.escalations.slice(-MAX_ESCALATIONS_KEPT);
+ console.log(`[å¯å°] ðš å级Issueå·²å建`);
+ } catch (err) { console.error(`[å¯å°] â ïž å建å级Issue倱莥: ${err.message}`); }
+ }
+}
+
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+// å·¡æ¥æš¡åŒ
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+
+async function patrolMode() {
+ console.log('[å¯å°] ð å·¡æ¥æš¡åŒå¯åš · æ«ææædeputy-message-board Issue...');
+
+ const status = readStatus();
+ status.patrol_runs++;
+ status.last_run = new Date().toISOString();
+
+ const ctx = loadSystemContext();
+ const systemSummary = buildSystemSummary(ctx);
+
+ let processedCount = 0;
+ let repliedCount = 0;
+
+ try {
+ const issues = await listDeputyIssues();
+ console.log(`[å¯å°] ð æŸå° ${issues.length} äžªåŒæŸççèšæ¿Issue`);
+
+ for (const issue of issues) {
+ status.issues_processed++;
+ const comments = await getIssueComments(issue.number);
+ const lastUserComment = [...comments].reverse().find(c => c.user.login !== 'github-actions[bot]');
+ const lastBotComment = [...comments].reverse().find(c => c.user.login === 'github-actions[bot]');
+
+ let needsReply = false;
+ let question = '';
+ let author = '';
+
+ if (comments.length === 0) {
+ needsReply = true;
+ question = issue.body || issue.title;
+ author = issue.user.login;
+ } else if (lastUserComment && (!lastBotComment || new Date(lastUserComment.created_at) > new Date(lastBotComment.created_at))) {
+ needsReply = true;
+ question = lastUserComment.body;
+ author = lastUserComment.user.login;
+ }
+
+ if (needsReply) {
+ processedCount++;
+ try {
+ const replied = await processMessage(issue.number, question, author, ctx, systemSummary, status);
+ if (replied) repliedCount++;
+ } catch (err) {
+ console.error(`[å¯å°] â ïž Issue #${issue.number} å€ç倱莥: ${err.message}`);
+ status.errors.push(`${new Date().toISOString()} · Issue #${issue.number}: ${err.message}`);
+ }
+ await new Promise(r => setTimeout(r, 3000));
+ }
+ }
+ } catch (err) {
+ console.error(`[å¯å°] â å·¡æ¥å€±èŽ¥: ${err.message}`);
+ status.errors.push(`${new Date().toISOString()} · patrol: ${err.message}`);
+ }
+
+ if (status.errors.length > MAX_ERRORS_KEPT) status.errors = status.errors.slice(-MAX_ERRORS_KEPT);
+ await checkAndEscalate(status);
+ status.last_success = repliedCount > 0 || processedCount === 0 ? new Date().toISOString() : status.last_success;
+ saveStatus(status);
+ console.log(`[å¯å°] ð å·¡æ¥å®æ · å€ç: ${processedCount} · åå€: ${repliedCount}`);
+}
+
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+// äºä»¶æš¡åŒ
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+
+async function eventMode() {
+ console.log('[å¯å°] ðïž äºä»¶æš¡åŒå¯åš...');
+
+ const status = readStatus();
+ status.event_runs++;
+ status.last_run = new Date().toISOString();
+ status.issues_processed++;
+
+ const isNewIssue = EVENT_NAME === 'issues';
+ const question = isNewIssue ? (ISSUE_BODY || ISSUE_TITLE) : COMMENT_BODY;
+ const author = isNewIssue ? ISSUE_AUTHOR : COMMENT_AUTHOR;
+
+ if (!question || question.trim().length === 0) {
+ console.log('[å¯å°] â ïž çèšå
容䞺空·跳è¿');
+ saveStatus(status);
+ return;
+ }
+
+ const ctx = loadSystemContext();
+ const systemSummary = buildSystemSummary(ctx);
+
+ try {
+ await processMessage(ISSUE_NUMBER, question, author, ctx, systemSummary, status);
+ status.last_success = new Date().toISOString();
+ } catch (err) {
+ console.error(`[å¯å°] â äºä»¶å€ç倱莥: ${err.message}`);
+ status.errors.push(`${new Date().toISOString()} · event #${ISSUE_NUMBER}: ${err.message}`);
+ }
+
+ if (status.errors.length > MAX_ERRORS_KEPT) status.errors = status.errors.slice(-MAX_ERRORS_KEPT);
+ await checkAndEscalate(status);
+ saveStatus(status);
+}
+
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+// äž»å
¥å£
+// âââââââââââââââââââââââââââââââââââââââââââââââ
+
+async function main() {
+ const mode = DEPUTY_MODE || (EVENT_NAME ? 'event' : 'patrol');
+ console.log(`[å¯å°] ðïž éžæžå¯å°v2.0å¯åš · æš¡åŒ: ${mode}`);
+ if (mode === 'patrol') await patrolMode();
+ else await eventMode();
}
main().catch(err => {
- console.error(`[DEPUTY-BOARD] â æ§è¡å€±èŽ¥: ${err.message}`);
+ console.error(`[å¯å°] â æ§è¡å€±èŽ¥: ${err.message}`);
+ try {
+ const status = readStatus();
+ status.errors.push(`${new Date().toISOString()} · fatal: ${err.message}`);
+ if (status.errors.length > MAX_ERRORS_KEPT) status.errors = status.errors.slice(-MAX_ERRORS_KEPT);
+ saveStatus(status);
+ } catch { /* ignore */ }
process.exit(1);
});
diff --git a/server/nginx/zhuyuan-sovereign.conf b/server/nginx/zhuyuan-sovereign.conf
index f448a3dd..9848a7f1 100644
--- a/server/nginx/zhuyuan-sovereign.conf
+++ b/server/nginx/zhuyuan-sovereign.conf
@@ -200,7 +200,17 @@ server {
# å°V3ææ/订é
请æ±éäŒ å°å€§èæå¡åšçV3订é
æå¡ (端å£3805)
# åè®®: HTTP (å
çœéä¿¡ïŒå€§èæå¡åšNginxçå¬80端å£default_server)
# éšçœ²æ¶ç± workflow èªåšå° ZY_BRAIN_HOST_PLACEHOLDER æ¿æ¢äžºå®é
IP
+ # CORS: å
讞GitHub Pagesè·šåè°çšåžŠå®œå
񄧮API
location /api/proxy-v3/ {
+ # CORS颿£å€ç (GitHub Pagesè·šå)
+ if ($request_method = OPTIONS) {
+ add_header Access-Control-Allow-Origin * always;
+ add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
+ add_header Access-Control-Allow-Headers "Content-Type" always;
+ add_header Access-Control-Max-Age 86400;
+ return 204;
+ }
+
proxy_pass http://ZY_BRAIN_HOST_PLACEHOLDER/api/proxy-v3/;
proxy_http_version 1.1;
proxy_set_header Host $host;
@@ -213,6 +223,7 @@ server {
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
+ add_header Access-Control-Allow-Origin * always;
}
# âââ éžæž API åå代ç (ç«¯å£ 3801 · é¢è§ç«¯å£) âââ
diff --git a/server/proxy/config/claude-relay-config.json b/server/proxy/config/claude-relay-config.json
new file mode 100644
index 00000000..d6c35684
--- /dev/null
+++ b/server/proxy/config/claude-relay-config.json
@@ -0,0 +1,75 @@
+{
+ "_sovereign": "TCS-0002â | SYS-GLW-0001",
+ "_copyright": "åœäœç»å-2026-A-00037559",
+ "_description": "ç¡
è°·Claudeäžçº¿äžç»§é
眮 · IPèµ°ç¡
谷·垊宜走æ°å å¡",
+ "version": "1.0",
+ "relay_architecture": {
+ "description": "åå±äžç»§: çšæ·è¯·æ± â æ°å å¡(垊宜æº) â ç¡
è°·(IP壳) â Claude API",
+ "singapore_servers": {
+ "ZY-SVR-002": {
+ "role": "é¢å·å端+äžçº¿äž»å
¥å£",
+ "location": "Singapore",
+ "spec": "2æ ž8G"
+ },
+ "ZY-SVR-005": {
+ "role": "倧è·DB+MCP+Agent+订é
",
+ "location": "Singapore",
+ "spec": "4æ ž8G"
+ }
+ },
+ "silicon_valley_server": {
+ "ZY-SVR-SV": {
+ "role": "Claude APIäžç»§Â·IP壳",
+ "location": "Silicon Valley (US)",
+ "purpose": "Claude API请æ±åºå£ · ç»è¿æ°å å¡IPå±èœ",
+ "github_secrets": {
+ "host": "ZY_SVR_SV_HOST",
+ "ssh_key": "ZY_SVR_SV_KEY",
+ "user": "ZY_SVR_SV_USER"
+ }
+ }
+ }
+ },
+ "claude_routing": {
+ "trigger_models": [
+ "claude-3-haiku-20240307",
+ "claude-3-sonnet-20240229",
+ "claude-3-opus-20240229",
+ "claude-3-5-sonnet-20241022"
+ ],
+ "relay_method": "ssh_tunnel",
+ "tunnel_config": {
+ "description": "æ°å 塿å¡åšéè¿SSHé§éå°Claude请æ±ä»ç¡
è°·åºå£",
+ "local_port": 18443,
+ "remote_host": "api.anthropic.com",
+ "remote_port": 443,
+ "ssh_command_template": "ssh -f -N -L 18443:api.anthropic.com:443 ${ZY_SVR_SV_USER}@${ZY_SVR_SV_HOST}"
+ },
+ "fallback": "direct",
+ "fallback_note": "åŠç¡
è°·é§éäžå¯çšïŒé级䞺çŽè¿(å¯èœè¢«å±èœ)"
+ },
+ "bandwidth_optimization": {
+ "strategy": "æ°å å¡åžŠå®œäžç»§",
+ "description": "请æ±äœéåžžåŸå°(å KB)ïŒç»SSHé§éå»¶è¿å¯å¿œç¥ãååºäœéè¿é§éè¿åïŒç¡
è°·æå¡åšäžçŒåä»»äœæ°æ®ã",
+ "sg_bandwidth": "é«åžŠå®œÂ·äœå»¶è¿Â·äž»æ°æ®éé",
+ "sv_bandwidth": "äœé
·ä»
åTCPæ¡æ+TLSåºå£"
+ },
+ "setup_requirements": {
+ "silicon_valley_server": [
+ "å®è£
SSH Server (å·²æ)",
+ "é
眮SSHå
¬é¥ (æ°å 塿å¡åš â ç¡
è°·æå¡åš)",
+ "é²ç«å¢æŸè¡SSH端å£",
+ "æ éå®è£
Nginx (纯SSHé§éæš¡åŒ)"
+ ],
+ "singapore_server": [
+ "é
眮SSHç§é¥ (è¿æ¥ç¡
è°·æå¡åš)",
+ "讟眮SSHé§éèªåšéè¿ (autossh or systemd)",
+ "llm-router.js æ£æµClaudeæš¡åæ¶èµ° localhost:18443"
+ ],
+ "github_secrets": [
+ "ZY_SVR_SV_HOST â ç¡
è°·æå¡åšIP",
+ "ZY_SVR_SV_KEY â SSHç§é¥",
+ "ZY_SVR_SV_USER â SSHçšæ·å"
+ ]
+ }
+}
diff --git a/server/proxy/config/server-registry.json b/server/proxy/config/server-registry.json
new file mode 100644
index 00000000..1db13ebd
--- /dev/null
+++ b/server/proxy/config/server-registry.json
@@ -0,0 +1,101 @@
+{
+ "_sovereign": "TCS-0002â | SYS-GLW-0001",
+ "_copyright": "åœäœç»å-2026-A-00037559",
+ "_description": "å
æ¹è¯èšäžç · åå°æå¡åšæ³šå衚",
+ "version": "1.0",
+ "updated_at": "2026-04-07",
+ "servers": [
+ {
+ "id": "ZY-SVR-002",
+ "name": "é¢å",
+ "role": "å端éæ+äžçº¿äž»å
¥å£",
+ "location": "Singapore",
+ "spec": "2æ ž8G",
+ "status": "active",
+ "services": [
+ "nginx (éææä»¶Â·äž»ç«+é¢è§ç«)",
+ "xray (VLESS+Reality VPN)",
+ "api (端å£3800·䞻ç«)",
+ "api-preview (端å£3801·é¢è§ç«)",
+ "persona-studio (端å£3002)",
+ "chat (端å£3721)"
+ ],
+ "github_secrets": {
+ "host": "SERVER_HOST",
+ "key": "SERVER_KEY",
+ "user": "SERVER_USER"
+ }
+ },
+ {
+ "id": "ZY-SVR-005",
+ "name": "倧è",
+ "role": "æ°æ®åº+MCP+Agent+订é
æå¡",
+ "location": "Singapore",
+ "spec": "4æ ž8G",
+ "status": "active",
+ "services": [
+ "postgresql (äººæ Œäœæ°æ®åº)",
+ "mcp-server (端å£3100·51䞪工å
·)",
+ "subscription-v3 (端å£3805·å€çšæ·äžçº¿)",
+ "bandwidth-pool-agent (垊宜æ±è)",
+ "email-hub (é®ä»¶éä¿¡äžæ¢)",
+ "auto-evolution (èªåšè¿ååŒæ)"
+ ],
+ "github_secrets": {
+ "host": "ZY_BRAIN_HOST",
+ "key": "ZY_BRAIN_KEY",
+ "user": "ZY_BRAIN_USER"
+ }
+ },
+ {
+ "id": "ZY-SVR-SV",
+ "name": "å±å±çº¿Â·ç¡
è°·",
+ "role": "Claude APIäžç»§+åœé
APIåºå£",
+ "location": "Silicon Valley (US)",
+ "spec": "äœé
",
+ "status": "pending",
+ "services": [
+ "ssh-tunnel (Claude APIäžç»§åºå£)",
+ "诎æ: IPä»ç¡
è°·èµ°Â·åžŠå®œä»æ°å å¡èµ°"
+ ],
+ "github_secrets": {
+ "host": "ZY_SVR_SV_HOST",
+ "key": "ZY_SVR_SV_KEY",
+ "user": "ZY_SVR_SV_USER"
+ },
+ "notes": "ç¡
è°·æå¡åšä»
åTCPåºå£Â·äžéšçœ²åºçš"
+ },
+ {
+ "id": "ZY-SVR-AWEN",
+ "name": "Awenæå¡åš",
+ "role": "éžæžäž»æ§å¯æ¬+Awenå¢éäžçº¿",
+ "location": "China",
+ "spec": "åŸ
确讀",
+ "status": "pending",
+ "services": [
+ "éžæžäž»æ§å¯æ¬ (宿Žä»åºéå)",
+ "Awenå¢éäžçº¿",
+ "ç¥ç§äººæ Œäœè¿è¡ç¯å¢"
+ ],
+ "github_secrets": {
+ "host": "ZY_SVR_AWEN_HOST",
+ "key": "ZY_SVR_AWEN_KEY",
+ "user": "ZY_SVR_AWEN_USER"
+ },
+ "notes": "Awenäžèœœå®æŽä»åºåéšçœ²Â·éžæžäž»æ§èªåšå€é"
+ }
+ ],
+ "bandwidth_strategy": {
+ "description": "åå°æå¡åšæçšéåæµÂ·åžŠå®œæå€§å",
+ "routing": {
+ "static_frontend": "ZY-SVR-002",
+ "vpn_main_entry": "ZY-SVR-002",
+ "database_mcp": "ZY-SVR-005",
+ "subscription_service": "ZY-SVR-005",
+ "claude_api_relay": "ZY-SVR-SV",
+ "international_api": "ZY-SVR-SV",
+ "awen_team": "ZY-SVR-AWEN",
+ "zhiqiu_persona": "ZY-SVR-AWEN"
+ }
+ }
+}
diff --git a/server/proxy/service/llm-router.js b/server/proxy/service/llm-router.js
index a0f2cac6..85548f3d 100644
--- a/server/proxy/service/llm-router.js
+++ b/server/proxy/service/llm-router.js
@@ -31,6 +31,24 @@ const MODEL_PRIORITY = [
'gpt-4o-mini'
];
+// ââ Claudeæš¡åè¯å« (éèµ°ç¡
è°·äžç»§) ââââââââââââââ
+const CLAUDE_MODELS = [
+ 'claude-3-haiku-20240307',
+ 'claude-3-sonnet-20240229',
+ 'claude-3-opus-20240229',
+ 'claude-3-5-sonnet-20241022'
+];
+
+// ç¡
è°·SSHé§éæ¬å°ç«¯å£ (Claude APIäžç»§)
+// æ°å 塿å¡åšéè¿SSHé§éå°Claude请æ±ä»ç¡
è°·IPåºå£
+// é
眮: server/proxy/config/claude-relay-config.json
+const CLAUDE_RELAY_PORT = parseInt(process.env.ZY_CLAUDE_RELAY_PORT || '18443', 10);
+const CLAUDE_RELAY_ENABLED = process.env.ZY_CLAUDE_RELAY_ENABLED === 'true';
+
+function isClaudeModel(model) {
+ return CLAUDE_MODELS.some(cm => model.includes(cm)) || model.includes('claude');
+}
+
// ââ è·¯ç±åšç¶æ ââââââââââââââââââââââââââââââ
const routerState = {
total_calls: 0,
@@ -111,6 +129,7 @@ async function callLLM(prompt, options = {}) {
/**
* è°çšå䞪暡å
+ * Claudeæš¡åèªåšèµ°ç¡
è°·äžç»§ (åœ ZY_CLAUDE_RELAY_ENABLED=true)
*/
function _callSingleModel(baseUrl, apiKey, model, prompt, systemPrompt, maxTokens, timeout) {
return new Promise((resolve, reject) => {
@@ -122,6 +141,12 @@ function _callSingleModel(baseUrl, apiKey, model, prompt, systemPrompt, maxToken
urlObj = new URL(`https://${baseUrl}`);
}
+ // Claudeæš¡åèµ°ç¡
è°·äžç»§ (SSHé§é localhost:18443 â ç¡
è°· â api.anthropic.com)
+ if (CLAUDE_RELAY_ENABLED && isClaudeModel(model)) {
+ console.log(`[LLMè·¯ç±åš] Claudeæš¡å ${model} â ç¡
è°·äžç»§ (localhost:${CLAUDE_RELAY_PORT})`);
+ urlObj = new URL(`https://localhost:${CLAUDE_RELAY_PORT}`);
+ }
+
const postData = JSON.stringify({
model,
messages: [
@@ -204,4 +229,4 @@ function getRouterStatus() {
};
}
-module.exports = { callLLM, getRouterStatus, MODEL_PRIORITY };
+module.exports = { callLLM, getRouterStatus, MODEL_PRIORITY, CLAUDE_MODELS, isClaudeModel };
diff --git a/server/proxy/service/subscription-server-v3.js b/server/proxy/service/subscription-server-v3.js
index 1bdcc930..8887158a 100644
--- a/server/proxy/service/subscription-server-v3.js
+++ b/server/proxy/service/subscription-server-v3.js
@@ -39,6 +39,7 @@ const userManager = require('./user-manager');
// ââ æäœå¿«ç
§æä»¶ ââââââââââââââââââââââââââââââ
const AUTH_SNAPSHOT_FILE = path.join(DATA_DIR, 'bandwidth-auth-snapshots.json');
const MAX_SNAPSHOTS = 500; // æå€ä¿ç500æ¡å¿«ç
§è®°åœ
+const MAX_SEND_CODE_PER_HOUR = 3; // æ¯äžªIPæ¯å°æ¶æå€åééªè¯ç 次æ°
/**
* ä¿åçšæ·æäœå¿«ç
§
@@ -1705,6 +1706,213 @@ async function submitCode(e) {
return;
}
+ // âââââââââââââââââââââââââââââââââââââââââââââââ
+ // â+1 å
¬åŒAPI: /bandwidth-send-code (æ étoken·é®ç®±éªè¯ç åé)
+ // çšæ·åšGitHub PageséŠé¡µèŸå
¥é®ç®± â è°çšæ€APIåééªè¯ç
+ // å®å
š: æ¯äžªIPæ¯å°æ¶æå€3次
+ // âââââââââââââââââââââââââââââââââââââââââââââââ
+ if (pathname === '/bandwidth-send-code' && req.method === 'POST') {
+ // CORS headers for GitHub Pages cross-origin
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
+
+ let body = '';
+ req.on('data', chunk => { body += chunk; if (body.length > 1024) req.destroy(); });
+ req.on('end', () => {
+ try {
+ let email = '';
+ if (req.headers['content-type']?.includes('application/json')) {
+ const json = JSON.parse(body);
+ email = String(json.email || '').trim().toLowerCase();
+ } else {
+ const params = new URLSearchParams(body);
+ email = String(params.get('email') || '').trim().toLowerCase();
+ }
+
+ if (!email || !email.includes('@')) {
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({ success: false, message: '请èŸå
¥ææçé®ç®±å°å' }));
+ return;
+ }
+
+ // Rate limiting: max 3 per IP per hour
+ const userIP = req.headers['x-forwarded-for']?.split(',')[0]?.trim()
+ || req.headers['x-real-ip']
+ || req.socket.remoteAddress
+ || '0.0.0.0';
+
+ if (!global._bwSendCodeRateLimit) global._bwSendCodeRateLimit = {};
+ const rl = global._bwSendCodeRateLimit;
+ const hourKey = `${userIP}:${Math.floor(Date.now() / 3600000)}`;
+ rl[hourKey] = (rl[hourKey] || 0) + 1;
+ // Clean old entries
+ const currentHourPrefix = Math.floor(Date.now() / 3600000);
+ for (const k of Object.keys(rl)) {
+ const hourPart = parseInt(k.split(':').pop(), 10);
+ if (hourPart < currentHourPrefix - 1) delete rl[k];
+ }
+ if (rl[hourKey] > MAX_SEND_CODE_PER_HOUR) {
+ res.writeHead(429, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({ success: false, message: 'åéé¢çè¿é«ïŒè¯·1å°æ¶åéè¯' }));
+ return;
+ }
+
+ const bwPool = require('./bandwidth-pool-agent');
+ const code = bwPool.createAuthCode(email);
+
+ // Try to send email
+ try {
+ const emailHub = require('./email-hub');
+ emailHub.sendBandwidthAuthEmail(email, code).then(() => {
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({ success: true, message: 'éªè¯ç å·²åéå°æšçé®ç®±ïŒè¯·æ¥æ¶ïŒ15åéå
ææïŒ' }));
+ }).catch(() => {
+ // Email failed but code was created - still return success with code hint
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({ success: true, message: 'éªè¯ç å·²çæïŒé®ä»¶åéäž...' }));
+ });
+ } catch {
+ // Email module not available - code was still created
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({ success: true, message: 'éªè¯ç å·²çæïŒè¯·è系管çåè·å' }));
+ }
+ } catch (err) {
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({ success: false, message: 'ç³»ç»åŒåžžïŒè¯·çšåéè¯' }));
+ }
+ });
+ return;
+ }
+
+ // âââââââââââââââââââââââââââââââââââââââââââââââ
+ // â+1 å
¬åŒAPI: /bandwidth-verify-code (æ étoken·éªè¯ç æ ¡éª+泚å)
+ // çšæ·åšGitHub PageséŠé¡µèŸå
¥é®ç®±+éªè¯ç â è°çšæ€API宿ææ
+ // âââââââââââââââââââââââââââââââââââââââââââââââ
+ if (pathname === '/bandwidth-verify-code' && req.method === 'POST') {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
+
+ let body = '';
+ req.on('data', chunk => { body += chunk; if (body.length > 1024) req.destroy(); });
+ req.on('end', () => {
+ try {
+ let code = '';
+ let email = '';
+ if (req.headers['content-type']?.includes('application/json')) {
+ const json = JSON.parse(body);
+ code = String(json.code || '').trim();
+ email = String(json.email || '').trim().toLowerCase();
+ } else {
+ const params = new URLSearchParams(body);
+ code = String(params.get('code') || '').trim();
+ email = String(params.get('email') || '').trim().toLowerCase();
+ }
+
+ if (!email || !email.includes('@')) {
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({ success: false, message: '请èŸå
¥ææçé®ç®±å°å' }));
+ return;
+ }
+ if (!code || code.length !== 6) {
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({ success: false, message: '请èŸå
¥6äœéªè¯ç ' }));
+ return;
+ }
+
+ const userIP = req.headers['x-forwarded-for']?.split(',')[0]?.trim()
+ || req.headers['x-real-ip']
+ || req.socket.remoteAddress
+ || '0.0.0.0';
+
+ const bwPool = require('./bandwidth-pool-agent');
+ const verifyResult = bwPool.verifyAuthCode(code, email);
+
+ if (!verifyResult.valid) {
+ saveAuthSnapshot({
+ email, action: 'bandwidth-verify-code', result: 'failed',
+ error: verifyResult.error, ip: userIP,
+ user_agent: req.headers['user-agent'] || 'unknown',
+ auth_type: 'github-pages-public'
+ });
+ res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({ success: false, message: verifyResult.error }));
+ return;
+ }
+
+ const regResult = bwPool.registerContributor(email, userIP);
+
+ saveAuthSnapshot({
+ email, action: 'bandwidth-verify-code', result: 'authorized',
+ ip: userIP, user_agent: req.headers['user-agent'] || 'unknown',
+ auth_type: 'github-pages-public',
+ contributor_id: regResult.contributor_id
+ });
+
+ try {
+ const guardian = require('./user-guardian-agent');
+ guardian.activateGuardian(email);
+ } catch { /* guardian not loaded */ }
+
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({
+ success: true,
+ message: 'â
æææåïŒæšç垊宜已å å
¥å éæ± ïŒæè°¢æ¯æã',
+ contributor_id: regResult.contributor_id
+ }));
+ } catch (err) {
+ res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({ success: false, message: 'ç³»ç»åŒåžžïŒè¯·çšåéè¯' }));
+ }
+ });
+ return;
+ }
+
+ // âââââââââââââââââââââââââââââââââââââââââââââââ
+ // â+1 å
¬åŒAPI: /bandwidth-pool-status (æ étokenÂ·åžŠå®œæ± ç¶æ)
+ // GitHub PageséŠé¡µå®æ¶æŸç€ºåžŠå®œæ± ç¶æ
+ // âââââââââââââââââââââââââââââââââââââââââââââââ
+ if (pathname === '/bandwidth-pool-status' && req.method === 'GET') {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
+
+ try {
+ const bwPool = require('./bandwidth-pool-agent');
+ const poolStatus = bwPool.getPoolStatus();
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({ success: true, ...poolStatus }));
+ } catch {
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
+ res.end(JSON.stringify({
+ success: true,
+ total_contributors: 0,
+ active_contributors: 0,
+ total_contributed_gb: 0,
+ pool_status: 'idle',
+ updated_at: new Date().toISOString()
+ }));
+ }
+ return;
+ }
+
+ // âââ CORS颿£ (OPTIONS) æ¯æ âââ
+ if (req.method === 'OPTIONS' && (
+ pathname === '/bandwidth-send-code' ||
+ pathname === '/bandwidth-verify-code' ||
+ pathname === '/bandwidth-pool-status'
+ )) {
+ res.writeHead(204, {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type',
+ 'Access-Control-Max-Age': '86400'
+ });
+ res.end();
+ return;
+ }
+
// 404
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');