aws-runtime-bridge 1.9.122 → 1.9.125

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +23 -2
  2. package/dist/adapter/AcodeSdkAdapter.js +1 -1
  3. package/dist/adapter/types.js +1 -1
  4. package/dist/browser/browser-ws-bridge.js +3 -3
  5. package/dist/config.d.ts +22 -2
  6. package/dist/config.js +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/remote-desktop/index.js +2 -2
  9. package/dist/routes/dashboard.js +576 -17
  10. package/dist/routes/git.d.ts +28 -0
  11. package/dist/routes/git.js +32 -31
  12. package/dist/routes/pty.js +3 -3
  13. package/dist/routes/runtime-binding.js +1 -1
  14. package/dist/routes/terminal.js +24 -24
  15. package/dist/services/access-control.d.ts +18 -0
  16. package/dist/services/access-control.js +5 -0
  17. package/dist/services/agent-process-manager.js +1 -1
  18. package/dist/services/auto-register.d.ts +2 -2
  19. package/dist/services/auto-register.js +1 -1
  20. package/dist/services/aws-client-agent-mcp.d.ts +2 -1
  21. package/dist/services/aws-client-agent-mcp.js +2 -2
  22. package/dist/services/cc-switch-sdk.js +1 -1
  23. package/dist/services/cli-commands.js +5 -5
  24. package/dist/services/fileSnapshotStore.js +1 -1
  25. package/dist/services/instance-ai-config-service.js +1 -1
  26. package/dist/services/instance-features.d.ts +2 -0
  27. package/dist/services/instance-features.js +1 -1
  28. package/dist/services/panel-auth.d.ts +3 -1
  29. package/dist/services/panel-auth.js +1 -1
  30. package/dist/services/startup-config-wizard.js +6 -6
  31. package/dist/services/system-metrics.js +1 -1
  32. package/dist/services/tunnel-client.d.ts +4 -0
  33. package/dist/services/tunnel-client.js +1 -1
  34. package/dist/services/update-notifier.js +2 -2
  35. package/dist/utils/config-write-queue.d.ts +3 -0
  36. package/dist/utils/config-write-queue.js +2 -0
  37. package/package/acode/dist/ask-user-question-tool.js +2 -2
  38. package/package/acode/dist/mcpToolNames.d.ts +2 -0
  39. package/package/acode/dist/mcpToolNames.js +1 -1
  40. package/package/acode/dist/meeting-debug-log.d.ts +1 -0
  41. package/package/acode/dist/meeting-debug-log.js +3 -0
  42. package/package/acode/dist/runtime.d.ts +2 -0
  43. package/package/acode/dist/runtime.js +12 -12
  44. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +12 -0
  45. package/package/aws-client-agent-mcp/dist/agent-client.js +1 -1
  46. package/package/aws-client-agent-mcp/dist/agent-client.test.js +179 -1
  47. package/package/aws-client-agent-mcp/dist/config.js +1 -1
  48. package/package/aws-client-agent-mcp/dist/http-client.d.ts +2 -0
  49. package/package/aws-client-agent-mcp/dist/http-client.js +1 -1
  50. package/package/aws-client-agent-mcp/dist/launch-config-tools.test.js +6 -2
  51. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +21 -0
  52. package/package/aws-client-agent-mcp/dist/mcp-server.js +28 -28
  53. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +604 -1
  54. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +10 -0
  55. package/package/aws-client-agent-mcp/dist/mcp-tools.js +12 -10
  56. package/package/aws-client-agent-mcp/dist/mcp-tools.test.js +38 -0
  57. package/package/aws-client-agent-mcp/dist/meeting-debug-log.d.ts +13 -0
  58. package/package/aws-client-agent-mcp/dist/meeting-debug-log.js +3 -0
  59. package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +2 -0
  60. package/package/aws-client-agent-mcp/dist/message-buffer.js +1 -1
  61. package/package/aws-client-agent-mcp/dist/message-buffer.test.js +60 -0
  62. package/package/aws-client-agent-mcp/dist/types.d.ts +11 -1
  63. package/package/aws-client-agent-mcp/dist/websocket-client.js +1 -1
  64. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import{Router as F}from"express";import p from"node:os";import{execFile as _}from"node:child_process";import{getRuntimeBindingPublicState as T,getRuntimePairingCode as V}from"../services/runtime-binding.js";import{getAgentProcessManager as y}from"../services/agent-process-manager.js";import{collectSystemMetrics as K}from"../services/system-metrics.js";import{loadPersistedSessions as W}from"../services/terminal-persistence.js";import{createLogger as G}from"../utils/logger.js";import{getBridgeVersion as q}from"../config.js";import{sdkSessions as J}from"./terminal.js";import{createSession as Y,getPanelCredentials as P,validateSession as E,destroySession as L,validateCredentials as U,hasConfiguredCredentials as x,isDefaultCredentials as $,setPanelCredentials as Q,getAllSessions as X,getActiveSessionCount as Z,checkLoginAllowed as ee,recordLoginFailure as te,recordLoginSuccess as ne}from"../services/panel-auth.js";import{getRecentLogs as se}from"../utils/logger.js";import{autoRegister as ae,getRegistrationState as D,getAutoRegisterConfigFilePath as oe}from"../services/auto-register.js";const w=G("dashboard");let k=null;const re=1e4;let R=null,S=null;const ie=3e4;async function N(){K().then(n=>{R={data:n,ts:Date.now()}}).catch(()=>{}),y().discoverAllProcesses().then(n=>{S={data:n,ts:Date.now()}}).catch(()=>{}),y().cleanupStaleRegistry().catch(()=>{})}N(),setInterval(N,ie);const M=q();async function de(){const n=await import("node:fs/promises"),e=await import("node:path"),t=await import("node:os"),s=e.join(String(process.env.AWS_RUNTIME_HOME_DIR||t.homedir()).trim()||t.homedir(),".aws-bridge","lifecycle-state.json");try{const i=await n.readFile(s,"utf8"),a=JSON.parse(i),o=Array.isArray(a.operations)?a.operations:[];return o.length===0?null:(o.sort((l,c)=>new Date(c.updatedAt).getTime()-new Date(l.updatedAt).getTime()),o[0]||null)}catch{return null}}const d=F(),z="panel_sid";function le(n){const e={};if(!n)return e;for(const t of n.split(";")){const s=t.indexOf("=");if(s===-1)continue;const i=t.slice(0,s).trim(),a=t.slice(s+1).trim();i&&(e[i]=a)}return e}function I(n){return le(n.headers.cookie)[z]}function H(n,e,t){const s=n.secure||n.headers["x-forwarded-proto"]==="https";return`${z}=${e}; HttpOnly; SameSite=Lax; Path=/${s?"; Secure":""}; Max-Age=${t}`}function g(n,e){const t=I(n);return t&&E(t)?!0:n.xhr||n.headers.accept?.includes("application/json")?(e.status(401).json({ok:!1,error:"unauthorized",needLogin:!0}),!1):(e.redirect("/login"),!1)}d.post("/runtime/login",(n,e)=>{const t=n.socket.remoteAddress||"unknown",s=ee(t);if(!s.allowed){e.status(429).set("Retry-After",String(s.retryAfterSec)).json({ok:!1,error:"too many failed login attempts, try again later",retryAfterSec:s.retryAfterSec});return}const{username:i,password:a}=n.body||{};if(!i||!a){e.status(400).json({ok:!1,error:"username and password required"});return}if(!U(String(i),String(a))){te(t),e.status(401).json({ok:!1,error:"invalid credentials"});return}ne(t);const o=Y(),l=!x();e.setHeader("Set-Cookie",H(n,o,86400)),e.json({ok:!0,needSetup:l})}),d.post("/runtime/logout",(n,e)=>{const t=I(n);t&&L(t),e.setHeader("Set-Cookie",H(n,"",0)),e.json({ok:!0})}),d.get("/runtime/credentials-status",(n,e)=>{if(!g(n,e))return;const t=P();e.json({ok:!0,isDefault:$(),configured:x(),username:t.username})}),d.post("/runtime/change-credentials",(n,e)=>{if(!g(n,e))return;const{username:t,password:s,passwordConfirm:i,oldPassword:a}=n.body||{};if(s&&String(s).trim()){if(String(s)!==String(i)){e.status(400).json({ok:!1,error:"passwords do not match"});return}if(x()){if(!a){e.status(400).json({ok:!1,error:"old password required"});return}const l=P();if(!U(l.username,String(a))){e.status(403).json({ok:!1,error:"old password is incorrect"});return}}}if(!t||!String(t).trim()){e.status(400).json({ok:!1,error:"username required"});return}const o=s&&String(s).trim()?String(s).trim():P().password;Q(String(t).trim(),o),e.json({ok:!0})}),d.get("/runtime/node-info",(n,e)=>{if(!g(n,e))return;const t=T(),s=D();e.json({ok:!0,hostname:p.hostname(),platform:p.platform(),arch:p.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version,bridgeVersion:M,configPath:oe(),homeDir:process.env.AWS_RUNTIME_HOME_DIR||p.homedir(),bindingStatus:t.status,paired:t.paired,instanceId:t.instanceId||s.instanceId,schedulerBaseUrl:t.schedulerBaseUrl,registered:s.registered,pairingCode:t.pairingCode,activeSessions:Z()})}),d.get("/runtime/logs",(n,e)=>{if(!g(n,e))return;const t=n.query.level||void 0,s=Math.min(Number(n.query.count)||200,500),a=t&&["debug","info","warn","error"].includes(t)?t:void 0,o=se(s,a);e.json({ok:!0,logs:o})}),d.post("/runtime/re-register",async(n,e)=>{if(g(n,e))try{const t=await ae();e.json({ok:t})}catch(t){const s=t;e.status(500).json({ok:!1,error:s.message})}}),d.get("/runtime/admin/sessions",(n,e)=>{if(!g(n,e))return;const t=X().map(s=>({token:s.token.slice(0,8)+"...",createdAt:new Date(s.createdAt).toISOString(),lastUsedAt:new Date(s.lastUsedAt).toISOString(),age:Math.floor((Date.now()-s.createdAt)/1e3)}));e.json({ok:!0,sessions:t})}),d.post("/runtime/admin/sessions/:token/destroy",(n,e)=>{g(n,e)&&(L(n.params.token),e.json({ok:!0}))});function ce(n){return new Promise((e,t)=>{const s=p.platform()==="win32",i=s?"taskkill":"kill",a=s?["/PID",String(n),"/F"]:["-9",String(n)];_(i,a,{timeout:5e3},o=>o?t(o):e())})}d.post("/runtime/kill-process",async(n,e)=>{if(!g(n,e))return;const t=Number(n.body.pid);if(!t||!Number.isInteger(t)||t<=0){e.status(400).json({ok:!1,error:"Invalid PID"});return}const s=y(),i=()=>{const o=new Set,l=S?.data??[];for(const c of l)typeof c.pid=="number"&&o.add(c.pid);for(const c of s.getAllManagedProcesses())typeof c.pid=="number"&&o.add(c.pid);return o};let a=i();if(a.size===0)try{S={data:await s.discoverAllProcesses(),ts:Date.now()},a=i()}catch{}if(!a.has(t)){w.warn("Rejected kill-process for unrecognized PID",{pid:t}),e.status(404).json({ok:!1,error:"PID is not a bridge-discovered or registered agent process"});return}try{await ce(t),w.info("Killed process",{pid:t}),e.json({ok:!0})}catch(o){const l=o;w.warn("Failed to kill process",{pid:t,error:l.message}),e.status(500).json({ok:!1,error:l.message})}}),d.get("/runtime/dashboard",async(n,e)=>{if(g(n,e)){if(k&&Date.now()-k.ts<re)return void e.json(k.data);try{const t=R?.data??null,s=S?.data??null;R||N();const[i,a,o]=await Promise.all([Promise.resolve(T()),Promise.resolve(D()),W().catch(()=>[])]),c=y().getAllManagedProcesses(),v=Array.from(J.entries()).map(([m,u])=>({sessionId:m,agentId:u.agentId,workspacePath:u.workspacePath,runtimeStatus:u.currentRuntimeStatus||"running",runtimeActionType:u.currentRuntimeActionType,runtimeActionLabel:u.currentRuntimeActionLabel,providerId:u.providerId}));let f={managed:0,orphan:0,unknown:0},C=[];if(s){f={managed:s.filter(r=>r.managementStatus==="managed").length,orphan:s.filter(r=>r.managementStatus==="orphan").length,unknown:s.filter(r=>r.managementStatus==="unknown").length};const m=new Map(c.map(r=>[r.pid,r]));C=s.map(r=>{const h=m.get(r.pid);return{agentId:r.agentId||h?.agentId,pid:r.pid,managementStatus:r.managementStatus,sessionId:h?.sessionId,state:h?.state,command:r.command,cwd:r.cwd,startedAt:r.startedAt}});const u={managed:0,orphan:1,unknown:2};C.sort((r,h)=>(u[r.managementStatus]??9)-(u[h.managementStatus]??9))}const j=new Set(v.map(m=>m.sessionId)),B=o.filter(m=>!j.has(m.sessionId)),b=i,O=await de(),A={ok:!0,collectedAt:new Date().toISOString(),bridge:{version:M,hostname:p.hostname(),platform:p.platform(),arch:p.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version},binding:{status:b.status,paired:b.paired,instanceId:b.instanceId,userId:b.userId,schedulerBaseUrl:b.schedulerBaseUrl,pairingCode:V()},registration:{registered:a.registered,instanceId:a.instanceId,lastAttempt:a.lastAttempt?.toISOString(),error:a.error},metrics:t,sessions:{active:v,activeCount:v.length,persisted:B,persistedCount:B.length,total:v.length+B.length},processes:{...f,total:f.managed+f.orphan+f.unknown,list:C,registryCount:c.length},lifecycleOperation:O};k={data:A,ts:Date.now()},e.json(A)}catch(t){const s=t;w.error("Dashboard aggregation failed:",s.message),e.status(500).json({ok:!1,error:s.message})}}}),d.get("/login",(n,e)=>{e.type("text/html; charset=utf-8"),e.send(ge)}),d.get("/",(n,e)=>{const t=I(n);if(!t||!E(t)){e.redirect("/login");return}if(!x()){e.redirect("/setup");return}const s=n.query.lang==="en"?"en":"zh";e.type("text/html; charset=utf-8"),e.send(ue.replace("{{LANG}}",s))}),d.get("/setup",(n,e)=>{const t=I(n);if(!t||!E(t)){e.redirect("/login");return}e.type("text/html; charset=utf-8"),e.send(pe)});const ue=`<!DOCTYPE html>
1
+ import{Router as $}from"express";import v from"node:fs";import K from"node:path";import I from"node:os";import{execFile as Y}from"node:child_process";import{getRuntimeBindingPublicState as H,getRuntimePairingCode as Q,normalizeSchedulerBaseUrl as X}from"../services/runtime-binding.js";import{getAgentProcessManager as C}from"../services/agent-process-manager.js";import{collectSystemMetrics as Z}from"../services/system-metrics.js";import{loadPersistedSessions as ee}from"../services/terminal-persistence.js";import{createLogger as te}from"../utils/logger.js";import{getBridgeVersion as ne,getRuntimeHomeDir as se,getUserHomeDir as re}from"../config.js";import{runExclusiveConfigWrite as O}from"../utils/config-write-queue.js";import{sdkSessions as ae}from"./terminal.js";import{createSession as oe,getPanelCredentials as N,validateSession as U,destroySession as z,validateCredentials as F,hasConfiguredCredentials as B,isDefaultCredentials as ie,setPanelCredentials as de,getAllSessions as le,getActiveSessionCount as ce,checkLoginAllowed as ue,recordLoginFailure as ge,recordLoginSuccess as pe}from"../services/panel-auth.js";import{getRecentLogs as fe}from"../utils/logger.js";import{autoRegister as me,getRegistrationState as _,getAutoRegisterConfigFilePath as E}from"../services/auto-register.js";import{getAccessControlEntries as be,isValidIpEntry as he,parseAllowIps as ve,refreshAccessControlCache as ye}from"../services/access-control.js";const x=te("dashboard");let T=null;const xe=1e4;let L=null,P=null;const we=3e4;async function D(){Z().then(t=>{L={data:t,ts:Date.now()}}).catch(()=>{}),C().discoverAllProcesses().then(t=>{P={data:t,ts:Date.now()}}).catch(()=>{}),C().cleanupStaleRegistry().catch(()=>{})}D(),setInterval(D,we);const V=ne();async function Se(){const t=await import("node:fs/promises"),n=(await import("node:path")).join(se(),".aws-bridge","lifecycle-state.json");try{const s=await t.readFile(n,"utf8"),i=JSON.parse(s),r=Array.isArray(i.operations)?i.operations:[];return r.length===0?null:(r.sort((a,o)=>new Date(o.updatedAt).getTime()-new Date(a.updatedAt).getTime()),r[0]||null)}catch{return null}}const g=$(),J="panel_sid";function Ie(t){const e={};if(!t)return e;for(const n of t.split(";")){const s=n.indexOf("=");if(s===-1)continue;const i=n.slice(0,s).trim(),r=n.slice(s+1).trim();i&&(e[i]=r)}return e}function A(t){return Ie(t.headers.cookie)[J]}function q(t,e,n){const s=t.secure||t.headers["x-forwarded-proto"]==="https";return`${J}=${e}; HttpOnly; SameSite=Lax; Path=/${s?"; Secure":""}; Max-Age=${n}`}function m(t,e){const n=A(t);return n&&U(n)?!0:t.xhr||t.headers.accept?.includes("application/json")?(e.status(401).json({ok:!1,error:"unauthorized",needLogin:!0}),!1):(e.redirect("/login"),!1)}g.post("/runtime/login",(t,e)=>{const n=t.socket.remoteAddress||"unknown",s=ue(n);if(!s.allowed){e.status(429).set("Retry-After",String(s.retryAfterSec)).json({ok:!1,error:"too many failed login attempts, try again later",retryAfterSec:s.retryAfterSec});return}const{username:i,password:r}=t.body||{};if(!i||!r){e.status(400).json({ok:!1,error:"username and password required"});return}if(!F(String(i),String(r))){ge(n),e.status(401).json({ok:!1,error:"invalid credentials"});return}pe(n);const a=oe(),o=!B();e.setHeader("Set-Cookie",q(t,a,86400)),e.json({ok:!0,needSetup:o})}),g.post("/runtime/logout",(t,e)=>{const n=A(t);n&&z(n),e.setHeader("Set-Cookie",q(t,"",0)),e.json({ok:!0})}),g.get("/runtime/credentials-status",(t,e)=>{if(!m(t,e))return;const n=N();e.json({ok:!0,isDefault:ie(),configured:B(),username:n.username})}),g.post("/runtime/change-credentials",async(t,e)=>{if(!m(t,e))return;const{username:n,password:s,passwordConfirm:i,oldPassword:r}=t.body||{};if(s&&String(s).trim()){if(String(s)!==String(i)){e.status(400).json({ok:!1,error:"passwords do not match"});return}if(B()){if(!r){e.status(400).json({ok:!1,error:"old password required"});return}const o=N();if(!F(o.username,String(r))){e.status(403).json({ok:!1,error:"old password is incorrect"});return}}}if(!n||!String(n).trim()){e.status(400).json({ok:!1,error:"username required"});return}const a=s&&String(s).trim()?String(s).trim():N().password;try{await de(String(n).trim(),a),e.json({ok:!0})}catch(o){const l=o;x.error("Failed to change panel credentials:",l.message),e.status(500).json({ok:!1,error:l.message})}});function W(t){const e=typeof t=="string"?t.trim():"";if(e)return e.length<8?"\u2022\u2022\u2022\u2022":"\u2022\u2022\u2022\u2022"+e.slice(-4)}function G(t){const e=Array.isArray(t.autoRegisterTargets)?t.autoRegisterTargets.filter(s=>!!(s&&typeof s=="object"&&!Array.isArray(s))):[];return e.length>0?e:["serverUrl","schedulerBaseUrl","userKey"].some(s=>!!String(t[s]??"").trim())?[t]:[]}const ke=["enabled","serverUrl","schedulerBaseUrl","tenantId","userKey","connectionKey","password","instanceName","instanceId","projectId","roleName","workspacePath","registerAddr","registerIp","virtualIp","tunnel","preferSameLanIp","maxRetries","retryInterval"];function Ce(t){const e={};for(const n of ke)t[n]!==void 0&&(e[n]=t[n]);return e}function Be(t,e){const n=String(t.connectionKey??"").trim()||String(e??"").trim();return{serverUrl:String(t.serverUrl??t.schedulerBaseUrl??"").trim(),tenantId:String(t.tenantId??"").trim(),instanceName:String(t.instanceName??"").trim(),instanceId:String(t.instanceId??"").trim()||void 0,registerAddr:String(t.registerAddr??"").trim(),tunnel:typeof t.tunnel=="boolean"?t.tunnel:void 0,userKeyMasked:W(t.userKey),userKeySet:!!String(t.userKey??"").trim(),connectionKeyMasked:W(n),connectionKeySet:!!n}}function R(t){return X(t)??String(t||"").trim()}g.get("/runtime/server-config",(t,e)=>{if(!m(t,e))return;const n=E();let s={};if(v.existsSync(n))try{s=JSON.parse(v.readFileSync(n,"utf-8"))}catch{e.status(500).json({ok:!1,error:"config.json is not valid JSON; fix it manually first"});return}const i=String(s.connectionKey??"").trim()||void 0,r=G(s).map(a=>Be(a,i));e.json({ok:!0,targets:r})});function M(t,e){const n=new Error(e);return n.status=t,n}g.post("/runtime/server-config",async(t,e)=>{if(!m(t,e))return;const n=(t.body||{}).targets;if(!Array.isArray(n)){e.status(400).json({ok:!1,error:"targets array is required"});return}const s=[];for(let r=0;r<n.length;r++){const a=n[r];if(!a||typeof a!="object"||Array.isArray(a)){e.status(400).json({ok:!1,error:`targets[${r}] must be an object`});return}const o=a,l=String(o.serverUrl??o.schedulerBaseUrl??"").trim();if(!l){e.status(400).json({ok:!1,error:`targets[${r}].serverUrl is required`});return}try{const d=new URL(l);if(!["http:","https:"].includes(d.protocol))throw new Error("protocol must be http or https")}catch{e.status(400).json({ok:!1,error:`targets[${r}].serverUrl must be a valid http(s) URL`});return}const f=o.tunnel;let p;if(f==="auto"||f===""||f==null)p=void 0;else if(f===!0||f==="true")p=!0;else if(f===!1||f==="false")p=!1;else{e.status(400).json({ok:!1,error:`targets[${r}].tunnel must be auto, true or false`});return}const h=String(o.originalServerUrl??"").trim();s.push({serverUrl:l,originalServerUrl:h&&h!==l?h:"",userKey:String(o.userKey??"").trim(),tenantId:String(o.tenantId??"").trim(),instanceName:String(o.instanceName??"").trim(),connectionKey:String(o.connectionKey??"").trim(),registerAddr:String(o.registerAddr??"").trim(),tunnel:p})}const i=new Set;for(const r of s){const a=R(r.serverUrl);if(i.has(a)){e.status(400).json({ok:!1,error:"duplicate serverUrl in targets"});return}i.add(a)}try{const r=E();await O(r,()=>{let a={};if(v.existsSync(r))try{a=JSON.parse(v.readFileSync(r,"utf-8"))}catch{throw M(500,"config.json is not valid JSON; fix it manually first")}const o=String(a.connectionKey??"").trim(),l=G(a),f=l.length>0&&l[0]===a,p=new Map(l.map(d=>[R(String(d.serverUrl??d.schedulerBaseUrl??"").trim()),f?Ce(d):d])),h=[];for(const d of s){let b;if(d.originalServerUrl){const w=R(d.originalServerUrl);b=p.get(w),b&&p.delete(w)}if(!b){const w=R(d.serverUrl);b=p.get(w),b&&p.delete(w)}const c=b?{...b}:{};if(c.serverUrl=d.serverUrl,c.tenantId=d.tenantId,c.instanceName=d.instanceName,c.registerAddr=d.registerAddr,d.tunnel===void 0?delete c.tunnel:c.tunnel=d.tunnel,d.userKey)c.userKey=d.userKey;else if(!String(c.userKey??"").trim())throw M(400,`userKey is required for ${d.serverUrl} (new target without inheritable userKey)`);d.connectionKey?c.connectionKey=d.connectionKey:!String(c.connectionKey??"").trim()&&o&&(c.connectionKey=o),h.push(c)}a.autoRegisterTargets=h;for(const d of["enabled","serverUrl","schedulerBaseUrl","tenantId","userKey","connectionKey","password","instanceName","instanceId","registerAddr","registerIp","virtualIp","tunnel"])delete a[d];v.mkdirSync(K.dirname(r),{recursive:!0}),v.writeFileSync(r,JSON.stringify(a,null,2),"utf-8")}),x.info("Server connection config updated via panel",{configPath:r,targetCount:s.length}),e.json({ok:!0})}catch(r){const a=r,o=Number(a.status)||500;o>=500&&x.error("Failed to save server connection config:",a.message),e.status(o).json({ok:!1,error:a.message})}}),g.get("/runtime/access-control",(t,e)=>{m(t,e)&&e.json({ok:!0,allowIps:be(),defaultAllowIps:["0.0.0.0"]})}),g.post("/runtime/access-control",async(t,e)=>{if(!m(t,e))return;const n=(t.body||{}).allowIps;if(typeof n!="string"&&!Array.isArray(n)){e.status(400).json({ok:!1,error:"allowIps must be a string or an array"});return}const s=ve(n),i=s.filter(r=>!he(r));if(i.length>0){e.status(400).json({ok:!1,error:`invalid IP/CIDR entries: ${i.join(", ")}`});return}try{const r=E();await O(r,()=>{let a={};if(v.existsSync(r))try{a=JSON.parse(v.readFileSync(r,"utf-8"))}catch{throw M(500,"config.json is not valid JSON; fix it manually first")}const o=a.accessControl&&typeof a.accessControl=="object"&&!Array.isArray(a.accessControl)?{...a.accessControl}:{};o.allowIps=s,a.accessControl=o,v.mkdirSync(K.dirname(r),{recursive:!0}),v.writeFileSync(r,JSON.stringify(a,null,2),"utf-8"),ye()}),x.info("Access control config updated via panel",{configPath:r,entryCount:s.length}),e.json({ok:!0,allowIps:s})}catch(r){const a=r,o=Number(a.status)||500;o>=500&&x.error("Failed to save access control config:",a.message),e.status(o).json({ok:!1,error:a.message})}}),g.get("/runtime/node-info",(t,e)=>{if(!m(t,e))return;const n=H(),s=_();e.json({ok:!0,hostname:I.hostname(),platform:I.platform(),arch:I.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version,bridgeVersion:V,configPath:E(),homeDir:re(),bindingStatus:n.status,paired:n.paired,instanceId:n.instanceId||s.instanceId,schedulerBaseUrl:n.schedulerBaseUrl,registered:s.registered,pairingCode:n.pairingCode,activeSessions:ce()})}),g.get("/runtime/logs",(t,e)=>{if(!m(t,e))return;const n=t.query.level||void 0,s=Math.min(Number(t.query.count)||200,500),r=n&&["debug","info","warn","error"].includes(n)?n:void 0,a=fe(s,r);e.json({ok:!0,logs:a})}),g.post("/runtime/re-register",async(t,e)=>{if(m(t,e))try{const n=await me();e.json({ok:n})}catch(n){const s=n;e.status(500).json({ok:!1,error:s.message})}}),g.get("/runtime/admin/sessions",(t,e)=>{if(!m(t,e))return;const n=le().map(s=>({token:s.token.slice(0,8)+"...",createdAt:new Date(s.createdAt).toISOString(),lastUsedAt:new Date(s.lastUsedAt).toISOString(),age:Math.floor((Date.now()-s.createdAt)/1e3)}));e.json({ok:!0,sessions:n})}),g.post("/runtime/admin/sessions/:token/destroy",(t,e)=>{m(t,e)&&(z(t.params.token),e.json({ok:!0}))});function Ee(t){return new Promise((e,n)=>{const s=I.platform()==="win32",i=s?"taskkill":"kill",r=s?["/PID",String(t),"/F"]:["-9",String(t)];Y(i,r,{timeout:5e3},a=>a?n(a):e())})}g.post("/runtime/kill-process",async(t,e)=>{if(!m(t,e))return;const n=Number(t.body.pid);if(!n||!Number.isInteger(n)||n<=0){e.status(400).json({ok:!1,error:"Invalid PID"});return}const s=C(),i=()=>{const a=new Set,o=P?.data??[];for(const l of o)typeof l.pid=="number"&&a.add(l.pid);for(const l of s.getAllManagedProcesses())typeof l.pid=="number"&&a.add(l.pid);return a};let r=i();if(r.size===0)try{P={data:await s.discoverAllProcesses(),ts:Date.now()},r=i()}catch{}if(!r.has(n)){x.warn("Rejected kill-process for unrecognized PID",{pid:n}),e.status(404).json({ok:!1,error:"PID is not a bridge-discovered or registered agent process"});return}try{await Ee(n),x.info("Killed process",{pid:n}),e.json({ok:!0})}catch(a){const o=a;x.warn("Failed to kill process",{pid:n,error:o.message}),e.status(500).json({ok:!1,error:o.message})}}),g.get("/runtime/dashboard",async(t,e)=>{if(m(t,e)){if(T&&Date.now()-T.ts<xe)return void e.json(T.data);try{const n=L?.data??null,s=P?.data??null;L||D();const[i,r,a]=await Promise.all([Promise.resolve(H()),Promise.resolve(_()),ee().catch(()=>[])]),l=C().getAllManagedProcesses(),f=Array.from(ae.entries()).map(([S,y])=>({sessionId:S,agentId:y.agentId,workspacePath:y.workspacePath,runtimeStatus:y.currentRuntimeStatus||"running",runtimeActionType:y.currentRuntimeActionType,runtimeActionLabel:y.currentRuntimeActionLabel,providerId:y.providerId}));let p={managed:0,orphan:0,unknown:0},h=[];if(s){p={managed:s.filter(u=>u.managementStatus==="managed").length,orphan:s.filter(u=>u.managementStatus==="orphan").length,unknown:s.filter(u=>u.managementStatus==="unknown").length};const S=new Map(l.map(u=>[u.pid,u]));h=s.map(u=>{const k=S.get(u.pid);return{agentId:u.agentId||k?.agentId,pid:u.pid,managementStatus:u.managementStatus,sessionId:k?.sessionId,state:k?.state,command:u.command,cwd:u.cwd,startedAt:u.startedAt}});const y={managed:0,orphan:1,unknown:2};h.sort((u,k)=>(y[u.managementStatus]??9)-(y[k.managementStatus]??9))}const d=new Set(f.map(S=>S.sessionId)),b=a.filter(S=>!d.has(S.sessionId)),c=i,w=await Se(),j={ok:!0,collectedAt:new Date().toISOString(),bridge:{version:V,hostname:I.hostname(),platform:I.platform(),arch:I.arch(),uptime:Math.floor(process.uptime()),nodeVersion:process.version},binding:{status:c.status,paired:c.paired,instanceId:c.instanceId,userId:c.userId,schedulerBaseUrl:c.schedulerBaseUrl,pairingCode:Q()},registration:{registered:r.registered,instanceId:r.instanceId,lastAttempt:r.lastAttempt?.toISOString(),error:r.error},metrics:n,sessions:{active:f,activeCount:f.length,persisted:b,persistedCount:b.length,total:f.length+b.length},processes:{...p,total:p.managed+p.orphan+p.unknown,list:h,registryCount:l.length},lifecycleOperation:w};T={data:j,ts:Date.now()},e.json(j)}catch(n){const s=n;x.error("Dashboard aggregation failed:",s.message),e.status(500).json({ok:!1,error:s.message})}}}),g.get("/login",(t,e)=>{e.type("text/html; charset=utf-8"),e.send(Pe)}),g.get("/",(t,e)=>{const n=A(t);if(!n||!U(n)){e.redirect("/login");return}if(!B()){e.redirect("/setup");return}const s=t.query.lang==="en"?"en":"zh";e.type("text/html; charset=utf-8"),e.send(Te.replace("{{LANG}}",s))}),g.get("/setup",(t,e)=>{const n=A(t);if(!n||!U(n)){e.redirect("/login");return}e.type("text/html; charset=utf-8"),e.send(Ae)});const Te=`<!DOCTYPE html>
2
2
  <html lang="{{LANG}}">
3
3
  <head>
4
4
  <meta charset="UTF-8">
@@ -280,6 +280,61 @@ import{Router as F}from"express";import p from"node:os";import{execFile as _}fro
280
280
  th, td { padding: 7px 10px; }
281
281
  .header h1 { font-size: 18px; }
282
282
  }
283
+
284
+ /* ===== Scrollbar (\u4E3B\u9898\u9002\u914D) ===== */
285
+ /* \u9875\u9762\u7EA7\u6EDA\u52A8\u6761\uFF1Apill \u5F62\u6001\uFF08\u8FB9\u6846 + background-clip\uFF09\uFF0C\u4E0E\u6DF1\u8272\u80CC\u666F\u878D\u4E3A\u4E00\u4F53 */
286
+ html {
287
+ scrollbar-width: thin;
288
+ scrollbar-color: color-mix(in srgb, var(--accent) 30%, transparent) var(--bg);
289
+ }
290
+ html::-webkit-scrollbar {
291
+ width: 10px;
292
+ background: var(--bg);
293
+ }
294
+ html::-webkit-scrollbar-thumb {
295
+ background: color-mix(in srgb, var(--accent) 30%, transparent);
296
+ border: 2px solid var(--bg);
297
+ border-radius: 5px;
298
+ background-clip: content-box;
299
+ }
300
+ html::-webkit-scrollbar-thumb:hover {
301
+ background: color-mix(in srgb, var(--accent) 50%, transparent);
302
+ background-clip: content-box;
303
+ }
304
+ /* \u5185\u90E8\u6EDA\u52A8\u5BB9\u5668\uFF08\u8868\u683C\u3001\u8BBE\u7F6E\u9762\u677F\u3001\u65E5\u5FD7\u5217\u8868\uFF09\uFF1A6px \u900F\u660E\u8F68\u9053 + accent \u80F6\u56CA\u6ED1\u5757 */
305
+ .table-wrap,
306
+ .settings-content,
307
+ .s-log-list,
308
+ .s-table-wrap {
309
+ scrollbar-width: thin;
310
+ scrollbar-color: color-mix(in srgb, var(--accent) 30%, transparent) transparent;
311
+ }
312
+ .table-wrap::-webkit-scrollbar,
313
+ .settings-content::-webkit-scrollbar,
314
+ .s-log-list::-webkit-scrollbar,
315
+ .s-table-wrap::-webkit-scrollbar {
316
+ width: 6px;
317
+ height: 6px;
318
+ }
319
+ .table-wrap::-webkit-scrollbar-track,
320
+ .settings-content::-webkit-scrollbar-track,
321
+ .s-log-list::-webkit-scrollbar-track,
322
+ .s-table-wrap::-webkit-scrollbar-track {
323
+ background: transparent;
324
+ }
325
+ .table-wrap::-webkit-scrollbar-thumb,
326
+ .settings-content::-webkit-scrollbar-thumb,
327
+ .s-log-list::-webkit-scrollbar-thumb,
328
+ .s-table-wrap::-webkit-scrollbar-thumb {
329
+ background: color-mix(in srgb, var(--accent) 30%, transparent);
330
+ border-radius: 3px;
331
+ }
332
+ .table-wrap::-webkit-scrollbar-thumb:hover,
333
+ .settings-content::-webkit-scrollbar-thumb:hover,
334
+ .s-log-list::-webkit-scrollbar-thumb:hover,
335
+ .s-table-wrap::-webkit-scrollbar-thumb:hover {
336
+ background: color-mix(in srgb, var(--accent) 50%, transparent);
337
+ }
283
338
  </style>
284
339
  </head>
285
340
  <body>
@@ -295,6 +350,7 @@ import{Router as F}from"express";import p from"node:os";import{execFile as _}fro
295
350
  </span>
296
351
  <span class="lang-btn" id="langBtn" onclick="toggleLang()">\u4E2D\u6587</span>
297
352
  <button onclick="openSettings()" id="settingsBtn" title="Settings / \u8BBE\u7F6E">&#x2699;</button>
353
+ <button onclick="doLogout()" id="logoutBtn" title="Logout / \u9000\u51FA\u767B\u5F55">&#x23FB;</button>
298
354
  <button onclick="refresh()" id="refreshBtn">&#x21bb;</button>
299
355
  </div>
300
356
  </div>
@@ -351,6 +407,7 @@ import{Router as F}from"express";import p from"node:os";import{execFile as _}fro
351
407
  <div class="settings-nav-item" data-page="node" onclick="switchSettingsPage('node')"><span id="navNode"></span></div>
352
408
  <div class="settings-nav-item" data-page="logs" onclick="switchSettingsPage('logs')"><span id="navLogs"></span></div>
353
409
  <div class="settings-nav-item" data-page="scheduler" onclick="switchSettingsPage('scheduler')"><span id="navScheduler"></span></div>
410
+ <div class="settings-nav-item" data-page="access" onclick="switchSettingsPage('access')"><span id="navAccess"></span></div>
354
411
  <div class="settings-nav-item" data-page="maintenance" onclick="switchSettingsPage('maintenance')"><span id="navMaintenance"></span></div>
355
412
  </div>
356
413
  <div class="settings-content">
@@ -417,15 +474,83 @@ import{Router as F}from"express";import p from"node:os";import{execFile as _}fro
417
474
  <!-- Scheduler page -->
418
475
  <div class="settings-page" id="pageScheduler">
419
476
  <div class="s-field">
420
- <label id="lblSchedUrl"></label>
421
- <div class="s-info-row"><span class="s-info-value" id="valSchedUrl">-</span></div>
477
+ <label id="lblSchedTargets"></label>
478
+ <div class="s-target-list" id="schedTargetList"></div>
479
+ </div>
480
+ <div class="s-actions s-actions-row" style="margin-bottom:18px">
481
+ <button class="s-btn s-btn-secondary" id="btnAddTarget" onclick="startAddTarget()"></button>
482
+ </div>
483
+ <div class="s-target-form" id="targetForm">
484
+ <div class="s-field">
485
+ <label id="lblSchedUrl"></label>
486
+ <input type="text" id="cfgServerUrl" placeholder="http://192.168.1.100:8080" autocomplete="off">
487
+ <span class="s-hint" id="hintSchedUrl"></span>
488
+ </div>
489
+ <div class="s-field">
490
+ <label id="lblSchedUserKey"></label>
491
+ <input type="password" id="cfgUserKey" autocomplete="new-password">
492
+ <span class="s-hint" id="hintSchedUserKey"></span>
493
+ </div>
494
+ <div class="s-field">
495
+ <label id="lblSchedTenantId"></label>
496
+ <input type="text" id="cfgTenantId" autocomplete="off">
497
+ <span class="s-hint" id="hintSchedTenantId"></span>
498
+ </div>
499
+ <div class="s-field">
500
+ <label id="lblSchedInstanceName"></label>
501
+ <input type="text" id="cfgInstanceName" autocomplete="off">
502
+ <span class="s-hint" id="hintSchedInstanceName"></span>
503
+ </div>
504
+ <div class="s-field">
505
+ <label id="lblSchedConnKey"></label>
506
+ <input type="password" id="cfgConnectionKey" autocomplete="new-password">
507
+ <span class="s-hint" id="hintSchedConnKey"></span>
508
+ </div>
509
+ <div class="s-field">
510
+ <label id="lblSchedRegisterAddr"></label>
511
+ <input type="text" id="cfgRegisterAddr" placeholder="192.168.1.25:18081" autocomplete="off">
512
+ <span class="s-hint" id="hintSchedRegisterAddr"></span>
513
+ </div>
514
+ <div class="s-field">
515
+ <label id="lblSchedTunnel"></label>
516
+ <select id="cfgTunnel">
517
+ <option value="auto" id="optTunnelAuto"></option>
518
+ <option value="true" id="optTunnelOn"></option>
519
+ <option value="false" id="optTunnelOff"></option>
520
+ </select>
521
+ <span class="s-hint" id="hintSchedTunnel"></span>
522
+ </div>
523
+ <div class="s-field">
524
+ <div class="s-info-row"><span class="s-info-label" id="lblSchedInstanceId"></span><span class="s-info-value" id="valSchedInstanceId">-</span></div>
525
+ <span class="s-hint" id="hintSchedInstanceId"></span>
526
+ </div>
527
+ <div class="s-msg" id="schedMsg"></div>
528
+ <div class="s-actions s-actions-row">
529
+ <button class="s-btn s-btn-primary" id="btnSaveServerConfig" onclick="saveServerConfig(false)"></button>
530
+ <button class="s-btn s-btn-danger" id="btnDeleteTarget" onclick="deleteTargetFromList()"></button>
531
+ </div>
422
532
  </div>
423
533
  <div class="s-field">
424
534
  <p class="s-desc" id="descSched"></p>
425
535
  </div>
426
- <div class="s-msg" id="schedMsg"></div>
536
+ <div class="s-actions s-actions-row">
537
+ <button class="s-btn s-btn-danger" id="btnSaveRestart" onclick="saveServerConfigAndRestart()"></button>
538
+ <button class="s-btn s-btn-secondary" id="btnReRegister" onclick="doReRegister()"></button>
539
+ </div>
540
+ </div>
541
+ <!-- Access control page -->
542
+ <div class="settings-page" id="pageAccess">
543
+ <div class="s-field">
544
+ <label id="lblAccessIps"></label>
545
+ <textarea id="cfgAllowIps" rows="4" placeholder="0.0.0.0" autocomplete="off" spellcheck="false"></textarea>
546
+ <span class="s-hint" id="hintAccessIps"></span>
547
+ </div>
548
+ <div class="s-msg" id="accessMsg"></div>
427
549
  <div class="s-actions">
428
- <button class="s-btn s-btn-primary" id="btnReRegister" onclick="doReRegister()"></button>
550
+ <button class="s-btn s-btn-primary" id="btnSaveAccessControl" onclick="saveAccessControl()"></button>
551
+ </div>
552
+ <div class="s-field" style="margin-top:18px">
553
+ <p class="s-desc" id="descAccess"></p>
429
554
  </div>
430
555
  </div>
431
556
  <!-- Maintenance page -->
@@ -478,6 +603,21 @@ import{Router as F}from"express";import p from"node:os";import{execFile as _}fro
478
603
  .s-field label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 6px; color: var(--text2); }
479
604
  .s-field input { width: 100%; padding: 9px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 14px; outline: none; }
480
605
  .s-field input:focus { border-color: var(--accent); }
606
+ .s-field select { width: 100%; padding: 9px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 14px; outline: none; cursor: pointer; }
607
+ .s-field select:focus { border-color: var(--accent); }
608
+ .s-field select option { background: var(--surface2); color: var(--text); }
609
+ .s-field textarea { width: 100%; padding: 9px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; color: var(--text); font-size: 14px; font-family: "Cascadia Code", "Fira Code", Consolas, monospace; outline: none; resize: vertical; }
610
+ .s-field textarea:focus { border-color: var(--accent); }
611
+ .s-actions-row { display: flex; gap: 10px; flex-wrap: wrap; }
612
+ .s-target-list { display: flex; flex-direction: column; gap: 8px; }
613
+ .s-target-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; }
614
+ .s-target-item.active { border-color: var(--accent); }
615
+ .s-target-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
616
+ .s-target-url { font-size: 14px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
617
+ .s-target-meta { font-size: 12px; opacity: 0.7; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
618
+ .s-target-actions { display: flex; gap: 6px; flex-shrink: 0; }
619
+ .s-target-form { border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 18px; background: var(--surface); }
620
+ .s-target-empty { padding: 14px; text-align: center; font-size: 13px; opacity: 0.6; border: 1px dashed var(--border); border-radius: 6px; }
481
621
  .s-hint { display: block; font-size: 11px; color: var(--text2); margin-top: 4px; }
482
622
  .s-msg { padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 14px; display: none; }
483
623
  .s-msg.error { display: block; background: rgba(240,84,84,0.12); color: var(--red); }
@@ -649,12 +789,53 @@ const T = {
649
789
  logEmpty: "\u6682\u65E0\u65E5\u5FD7",
650
790
  // \u8C03\u5EA6\u4E2D\u5FC3
651
791
  schedTitle: "\u8C03\u5EA6\u4E2D\u5FC3",
652
- schedUrl: "\u670D\u52A1\u5730\u5740",
792
+ schedTargets: "\u8C03\u5EA6\u4E2D\u5FC3\u5217\u8868\uFF08\u652F\u6301\u591A\u4E2A\uFF0C\u4FDD\u5B58\u540E\u5E76\u884C\u8FDE\u63A5\uFF09",
793
+ schedNoTargets: "\u5C1A\u672A\u914D\u7F6E\uFF0C\u70B9\u51FB\u4E0B\u65B9\u6309\u94AE\u6DFB\u52A0",
794
+ schedAddTarget: "\uFF0B \u6DFB\u52A0\u8C03\u5EA6\u4E2D\u5FC3",
795
+ schedEditTarget: "\u7F16\u8F91",
796
+ schedDeleteTarget: "\u5220\u9664",
797
+ schedConfirmDelete: "\u5220\u9664\u8BE5\u8C03\u5EA6\u4E2D\u5FC3\u914D\u7F6E\uFF1F",
798
+ schedFormNew: "\u65B0\u589E\u8C03\u5EA6\u4E2D\u5FC3",
799
+ schedUrl: "\u670D\u52A1\u5730\u5740 (serverUrl)",
800
+ schedUrlHint: "\u5FC5\u586B\uFF0C\u4EE5 http:// \u6216 https:// \u5F00\u5934\uFF1B\u5730\u5740\u53D8\u66F4\u65F6 API Key \u4F1A\u81EA\u52A8\u6CBF\u7528",
801
+ schedUserKey: "\u7528\u6237 API Key (userKey)",
802
+ schedUserKeyHint: "\u5FC5\u586B\uFF1B\u5DF2\u914D\u7F6E\u65F6\u7559\u7A7A\u8868\u793A\u4FDD\u6301\u4E0D\u53D8",
803
+ schedTenantId: "\u79DF\u6237 ID\uFF08\u53EF\u9009\uFF09",
804
+ schedTenantIdHint: "\u7559\u7A7A\u7531 server \u6839\u636E userKey \u81EA\u52A8\u63A8\u5BFC",
805
+ schedInstanceName: "\u5B9E\u4F8B\u540D\u79F0\uFF08\u53EF\u9009\uFF09",
806
+ schedInstanceNameHint: "\u7559\u7A7A\u4F7F\u7528\u4E3B\u673A\u540D",
807
+ schedConnKey: "\u8FDE\u63A5\u5BC6\u94A5\uFF08\u53EF\u9009\uFF09",
808
+ schedConnKeyHint: "server \u8FDE\u63A5\u8BE5 bridge \u7684\u5BC6\u94A5\uFF1B\u5DF2\u914D\u7F6E\u65F6\u7559\u7A7A\u8868\u793A\u4FDD\u6301\u4E0D\u53D8",
809
+ schedRegisterAddr: "\u6CE8\u518C\u5730\u5740\uFF08\u53EF\u9009\uFF09",
810
+ schedRegisterAddrHint: "\u76F4\u8FDE\u6A21\u5F0F\u4E0B\u7684\u5730\u5740\uFF0C\u5982 192.168.1.25:18081\uFF1B\u6E05\u7A7A\u8868\u793A\u4E0D\u6307\u5B9A",
811
+ schedTunnel: "\u96A7\u9053\u6A21\u5F0F",
812
+ schedTunnelHint: "\u81EA\u52A8\uFF1A\u672A\u914D\u7F6E\u6CE8\u518C\u5730\u5740\u65F6\u81EA\u52A8\u542F\u7528\u96A7\u9053\uFF1B\u542F\u7528\uFF1A\u7ECF WebSocket \u53CD\u5411\u8FDE\u63A5 server\uFF1B\u7981\u7528\uFF1A\u76F4\u8FDE",
813
+ schedTunnelAuto: "\u81EA\u52A8",
814
+ schedTunnelOn: "\u542F\u7528",
815
+ schedTunnelOff: "\u7981\u7528",
816
+ schedInstanceId: "\u5B9E\u4F8B ID",
817
+ schedInstanceIdHint: "\u7F16\u8F91\u6539\u5740\u65F6\u81EA\u52A8\u7EE7\u627F\uFF08\u540C\u4E00\u5B9E\u4F8B\u6362\u5730\u5740\uFF09\uFF0C\u65B0\u589E\u76EE\u6807\u6CE8\u518C\u6210\u529F\u540E\u81EA\u52A8\u56DE\u586B",
818
+ schedSave: "\u4FDD\u5B58\u914D\u7F6E",
819
+ schedSaving: "\u4FDD\u5B58\u4E2D...",
820
+ schedSaved: "\u914D\u7F6E\u5DF2\u4FDD\u5B58",
821
+ schedSavedRegistering: "\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u6B63\u5728\u91CD\u65B0\u6CE8\u518C...",
822
+ schedSaveFail: "\u4FDD\u5B58\u5931\u8D25",
823
+ schedSaveAndRestart: "\u4FDD\u5B58\u5E76\u91CD\u542F",
824
+ schedSaveRestartConfirm: "\u4FDD\u5B58\u914D\u7F6E\u5E76\u91CD\u542F Bridge\uFF1F\u91CD\u542F\u671F\u95F4\u670D\u52A1\u4F1A\u77ED\u6682\u4E2D\u65AD\uFF0C\u8FD0\u884C\u4E2D\u7684 Agent \u4F1A\u88AB\u4FDD\u7559\u3002",
653
825
  schedReRegister: "\u91CD\u65B0\u6CE8\u518C",
654
826
  schedReRegistering: "\u6CE8\u518C\u4E2D...",
655
827
  schedReRegistered: "\u91CD\u65B0\u6CE8\u518C\u6210\u529F",
656
828
  schedReRegisterFail: "\u91CD\u65B0\u6CE8\u518C\u5931\u8D25",
657
- schedDesc: "\u5411\u8C03\u5EA6\u4E2D\u5FC3\u53D1\u8D77\u91CD\u65B0\u6CE8\u518C\uFF0C\u5EFA\u8BAE\u5728\u8FDE\u63A5\u5F02\u5E38\u65F6\u4F7F\u7528\u3002",
829
+ schedDesc: "\u4FDD\u5B58\u540E\u53EF\u70B9\u51FB\u201C\u91CD\u65B0\u6CE8\u518C\u201D\u7ACB\u5373\u5E94\u7528\u65B0\u914D\u7F6E\uFF1B\u96A7\u9053\u6A21\u5F0F\u7B49\u90E8\u5206\u53D8\u66F4\u9700\u91CD\u542F Bridge \u751F\u6548\u3002",
830
+ // \u8BBF\u95EE\u63A7\u5236
831
+ accTitle: "\u8BBF\u95EE\u63A7\u5236",
832
+ accAllowIps: "\u5141\u8BB8\u8BBF\u95EE\u7684 IP / \u7F51\u6BB5",
833
+ accAllowIpsHint: "\u591A\u4E2A\u7528\u9017\u53F7\u6216\u6362\u884C\u5206\u9694\uFF0C\u652F\u6301\u5355\u4E2A IP \u4E0E CIDR \u7F51\u6BB5\uFF08\u5982 192.168.1.0/24\uFF09\uFF1B\u586B 0.0.0.0 \u6216 * \u8868\u793A\u5BF9\u6240\u6709\u4EBA\u5F00\u653E\uFF1B\u7559\u7A7A\u9ED8\u8BA4\u5373 0.0.0.0 \u5168\u5F00\u653E",
834
+ accDesc: "\u4FDD\u5B58\u540E\u7ACB\u5373\u751F\u6548\uFF0C\u65E0\u9700\u91CD\u542F\u3002\u672C\u673A\uFF08127.0.0.1\uFF09\u59CB\u7EC8\u5141\u8BB8\u8BBF\u95EE\uFF1B\u76F4\u8FDE\u6A21\u5F0F\u7684\u8C03\u5EA6\u4E2D\u5FC3\u82E5\u6765\u81EA\u5916\u90E8\u5730\u5740\uFF0C\u9700\u5C06\u5176 IP \u52A0\u5165\u767D\u540D\u5355\uFF0C\u5426\u5219\u4F1A\u88AB\u62D2\u7EDD\u3002",
835
+ accSave: "\u4FDD\u5B58",
836
+ accSaving: "\u4FDD\u5B58\u4E2D...",
837
+ accSaved: "\u5DF2\u4FDD\u5B58\u5E76\u751F\u6548",
838
+ accSaveFail: "\u4FDD\u5B58\u5931\u8D25",
658
839
  // \u72B6\u6001\u6587\u672C
659
840
  stHealthy: "\u5065\u5EB7",
660
841
  stError: "\u5F02\u5E38",
@@ -778,12 +959,53 @@ const T = {
778
959
  logEmpty: "No logs yet",
779
960
  // Scheduler
780
961
  schedTitle: "Scheduler",
781
- schedUrl: "Server URL",
962
+ schedTargets: "Scheduler targets (multiple allowed, connected in parallel)",
963
+ schedNoTargets: "None configured. Click the button below to add one.",
964
+ schedAddTarget: "+ Add scheduler",
965
+ schedEditTarget: "Edit",
966
+ schedDeleteTarget: "Delete",
967
+ schedConfirmDelete: "Delete this scheduler configuration?",
968
+ schedFormNew: "New scheduler",
969
+ schedUrl: "Server URL (serverUrl)",
970
+ schedUrlHint: "Required, must start with http:// or https://; the API key is kept when the URL changes",
971
+ schedUserKey: "User API Key (userKey)",
972
+ schedUserKeyHint: "Required; leave blank to keep the configured value",
973
+ schedTenantId: "Tenant ID (optional)",
974
+ schedTenantIdHint: "Leave blank to auto-derive from userKey on the server",
975
+ schedInstanceName: "Instance name (optional)",
976
+ schedInstanceNameHint: "Leave blank to use the hostname",
977
+ schedConnKey: "Connection key (optional)",
978
+ schedConnKeyHint: "Key for the server to connect to this bridge; leave blank to keep the configured value",
979
+ schedRegisterAddr: "Register address (optional)",
980
+ schedRegisterAddrHint: "Address in direct mode, e.g. 192.168.1.25:18081; clear to unset",
981
+ schedTunnel: "Tunnel mode",
982
+ schedTunnelHint: "Auto: tunnel is enabled when no register address is set; On: reverse WebSocket connection to the server; Off: direct connection",
983
+ schedTunnelAuto: "Auto",
984
+ schedTunnelOn: "On",
985
+ schedTunnelOff: "Off",
986
+ schedInstanceId: "Instance ID",
987
+ schedInstanceIdHint: "Inherited automatically when editing the URL (same instance, new address); backfilled after a new target registers successfully",
988
+ schedSave: "Save config",
989
+ schedSaving: "Saving...",
990
+ schedSaved: "Configuration saved",
991
+ schedSavedRegistering: "Configuration saved, re-registering...",
992
+ schedSaveFail: "Failed to save",
993
+ schedSaveAndRestart: "Save & restart",
994
+ schedSaveRestartConfirm: "Save the configuration and restart the bridge? The service will be briefly interrupted; running agents are preserved.",
782
995
  schedReRegister: "Re-register",
783
996
  schedReRegistering: "Registering...",
784
997
  schedReRegistered: "Re-registered successfully",
785
998
  schedReRegisterFail: "Re-registration failed",
786
- schedDesc: "Re-register with the scheduler. Use when connection is abnormal.",
999
+ schedDesc: "After saving, click 'Re-register' to apply the new config immediately; some changes such as tunnel mode require a bridge restart.",
1000
+ // Access control
1001
+ accTitle: "Access Control",
1002
+ accAllowIps: "Allowed IPs / networks",
1003
+ accAllowIpsHint: "Separate entries with commas or new lines; single IPs and CIDR blocks (e.g. 192.168.1.0/24) are supported; 0.0.0.0 or * allows everyone; empty defaults to 0.0.0.0 (allow all)",
1004
+ accDesc: "Takes effect immediately after saving, no restart needed. Loopback (127.0.0.1) is always allowed; if a direct-mode scheduler connects from an external address, add its IP to the list or it will be rejected.",
1005
+ accSave: "Save",
1006
+ accSaving: "Saving...",
1007
+ accSaved: "Saved and applied",
1008
+ accSaveFail: "Failed to save",
787
1009
  // \u72B6\u6001\u6587\u672C
788
1010
  stHealthy: "Healthy",
789
1011
  stError: "Error",
@@ -1092,6 +1314,15 @@ function toggleUnknown() {
1092
1314
  }
1093
1315
 
1094
1316
  // \u2500\u2500 \u8BF7\u6C42\u4E0E\u8F6E\u8BE2 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1317
+ // \u9000\u51FA\u767B\u5F55\uFF1A\u9500\u6BC1\u9762\u677F\u4F1A\u8BDD\u540E\u8DF3\u8F6C\u767B\u5F55\u9875
1318
+ function doLogout() {
1319
+ fetch("/runtime/logout", { method: "POST", headers: { "Content-Type": "application/json" } })
1320
+ .catch(function() {})
1321
+ .then(function() {
1322
+ location.href = "/login";
1323
+ });
1324
+ }
1325
+
1095
1326
  async function refresh() {
1096
1327
  try {
1097
1328
  const resp = await fetch("/runtime/dashboard");
@@ -1258,15 +1489,44 @@ function renderSettingsUI() {
1258
1489
  document.getElementById("lblNodeActiveSessions").textContent = t("nodeActiveSessions");
1259
1490
  document.getElementById("lblLogAutoRefresh").textContent = t("logAutoRefresh");
1260
1491
  document.getElementById("logEmpty").textContent = t("logEmpty");
1492
+ document.getElementById("lblSchedTargets").textContent = t("schedTargets");
1493
+ document.getElementById("btnAddTarget").textContent = t("schedAddTarget");
1494
+ document.getElementById("btnDeleteTarget").textContent = t("schedDeleteTarget");
1261
1495
  document.getElementById("lblSchedUrl").textContent = t("schedUrl");
1496
+ document.getElementById("hintSchedUrl").textContent = t("schedUrlHint");
1497
+ document.getElementById("lblSchedUserKey").textContent = t("schedUserKey");
1498
+ document.getElementById("hintSchedUserKey").textContent = t("schedUserKeyHint");
1499
+ document.getElementById("lblSchedTenantId").textContent = t("schedTenantId");
1500
+ document.getElementById("hintSchedTenantId").textContent = t("schedTenantIdHint");
1501
+ document.getElementById("lblSchedInstanceName").textContent = t("schedInstanceName");
1502
+ document.getElementById("hintSchedInstanceName").textContent = t("schedInstanceNameHint");
1503
+ document.getElementById("lblSchedConnKey").textContent = t("schedConnKey");
1504
+ document.getElementById("hintSchedConnKey").textContent = t("schedConnKeyHint");
1505
+ document.getElementById("lblSchedRegisterAddr").textContent = t("schedRegisterAddr");
1506
+ document.getElementById("hintSchedRegisterAddr").textContent = t("schedRegisterAddrHint");
1507
+ document.getElementById("lblSchedTunnel").textContent = t("schedTunnel");
1508
+ document.getElementById("hintSchedTunnel").textContent = t("schedTunnelHint");
1509
+ document.getElementById("optTunnelAuto").textContent = t("schedTunnelAuto");
1510
+ document.getElementById("optTunnelOn").textContent = t("schedTunnelOn");
1511
+ document.getElementById("optTunnelOff").textContent = t("schedTunnelOff");
1512
+ document.getElementById("lblSchedInstanceId").textContent = t("schedInstanceId");
1513
+ document.getElementById("hintSchedInstanceId").textContent = t("schedInstanceIdHint");
1514
+ document.getElementById("btnSaveServerConfig").textContent = t("schedSave");
1515
+ document.getElementById("btnSaveRestart").textContent = t("schedSaveAndRestart");
1262
1516
  document.getElementById("descSched").textContent = t("schedDesc");
1263
1517
  document.getElementById("btnReRegister").textContent = t("schedReRegister");
1518
+ document.getElementById("navAccess").textContent = t("accTitle");
1519
+ document.getElementById("lblAccessIps").textContent = t("accAllowIps");
1520
+ document.getElementById("hintAccessIps").textContent = t("accAllowIpsHint");
1521
+ document.getElementById("descAccess").textContent = t("accDesc");
1522
+ document.getElementById("btnSaveAccessControl").textContent = t("accSave");
1264
1523
  }
1265
1524
  function switchSettingsPage(page) {
1266
1525
  document.querySelectorAll(".settings-nav-item").forEach(function(el) { el.classList.remove("active"); });
1267
1526
  document.querySelectorAll(".settings-page").forEach(function(el) { el.classList.remove("active"); });
1268
1527
  document.querySelector('.settings-nav-item[data-page="' + page + '"]').classList.add("active");
1269
1528
  document.getElementById('page' + page.charAt(0).toUpperCase() + page.slice(1)).classList.add("active");
1529
+ if (page === "access") refreshAccessControl();
1270
1530
  }
1271
1531
  function openSettings() {
1272
1532
  if (settingsLang !== lang) { renderSettingsUI(); settingsLang = lang; }
@@ -1428,6 +1688,267 @@ document.addEventListener("change", function(e) {
1428
1688
  });
1429
1689
 
1430
1690
  // \u2500\u2500 Scheduler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1691
+ // \u591A\u8C03\u5EA6\u4E2D\u5FC3\u7BA1\u7406\uFF1AschedTargets \u7F13\u5B58\u5F53\u524D\u5217\u8868\uFF08GET \u89C6\u56FE\uFF09\uFF0CschedEditIndex \u4E3A\u8868\u5355\u6B63\u5728\u7F16\u8F91\u7684\u4E0B\u6807\uFF08-1 = \u65B0\u589E\uFF09
1692
+ var schedTargets = [];
1693
+ var schedEditIndex = -1;
1694
+
1695
+ // \u6E32\u67D3\u76EE\u6807\u5217\u8868\uFF08\u5361\u7247\uFF1AURL + \u5143\u4FE1\u606F\uFF0C\u70B9\u51FB\u8FDB\u5165\u7F16\u8F91\uFF09
1696
+ function renderSchedTargetList() {
1697
+ var list = document.getElementById("schedTargetList");
1698
+ list.innerHTML = "";
1699
+ if (!schedTargets.length) {
1700
+ var empty = document.createElement("div");
1701
+ empty.className = "s-target-empty";
1702
+ empty.textContent = t("schedNoTargets");
1703
+ list.appendChild(empty);
1704
+ return;
1705
+ }
1706
+ for (var i = 0; i < schedTargets.length; i++) {
1707
+ (function(idx) {
1708
+ var item = schedTargets[idx];
1709
+ var row = document.createElement("div");
1710
+ row.className = "s-target-item" + (idx === schedEditIndex ? " active" : "");
1711
+ var main = document.createElement("div");
1712
+ main.className = "s-target-main";
1713
+ var url = document.createElement("div");
1714
+ url.className = "s-target-url";
1715
+ url.textContent = item.serverUrl;
1716
+ var meta = document.createElement("div");
1717
+ meta.className = "s-target-meta";
1718
+ var parts = [];
1719
+ if (item.instanceName) parts.push(item.instanceName);
1720
+ if (item.instanceId) parts.push("ID: " + item.instanceId);
1721
+ if (item.tunnel === true) parts.push("tunnel");
1722
+ else if (item.tunnel === false && item.registerAddr) parts.push("direct");
1723
+ meta.textContent = parts.join(" \xB7 ");
1724
+ main.appendChild(url);
1725
+ main.appendChild(meta);
1726
+ var actions = document.createElement("div");
1727
+ actions.className = "s-target-actions";
1728
+ var editBtn = document.createElement("button");
1729
+ editBtn.className = "s-btn s-btn-sm s-btn-secondary";
1730
+ editBtn.textContent = t("schedEditTarget");
1731
+ editBtn.onclick = function(ev) { ev.stopPropagation(); loadTargetToForm(idx); };
1732
+ actions.appendChild(editBtn);
1733
+ row.appendChild(main);
1734
+ row.appendChild(actions);
1735
+ row.onclick = function() { loadTargetToForm(idx); };
1736
+ list.appendChild(row);
1737
+ })(i);
1738
+ }
1739
+ }
1740
+
1741
+ // \u5C06\u6307\u5B9A target \u8F7D\u5165\u8868\u5355\uFF08\u7F16\u8F91\u6A21\u5F0F\uFF09\uFF1A\u5BC6\u94A5\u7559\u7A7A=\u4FDD\u6301\u4E0D\u53D8\uFF08placeholder \u663E\u793A\u63A9\u7801\uFF09
1742
+ function loadTargetToForm(idx) {
1743
+ schedEditIndex = idx;
1744
+ var item = schedTargets[idx];
1745
+ document.getElementById("cfgServerUrl").value = item.serverUrl || "";
1746
+ document.getElementById("cfgTenantId").value = item.tenantId || "";
1747
+ document.getElementById("cfgInstanceName").value = item.instanceName || "";
1748
+ document.getElementById("cfgRegisterAddr").value = item.registerAddr || "";
1749
+ document.getElementById("cfgTunnel").value =
1750
+ item.tunnel === true ? "true" : (item.tunnel === false ? "false" : "auto");
1751
+ var uk = document.getElementById("cfgUserKey");
1752
+ var ck = document.getElementById("cfgConnectionKey");
1753
+ uk.value = "";
1754
+ ck.value = "";
1755
+ uk.placeholder = item.userKeyMasked || "";
1756
+ ck.placeholder = item.connectionKeyMasked || "";
1757
+ document.getElementById("valSchedInstanceId").textContent = item.instanceId || "-";
1758
+ document.getElementById("btnDeleteTarget").style.display = "";
1759
+ renderSchedTargetList();
1760
+ }
1761
+
1762
+ // \u8FDB\u5165\u65B0\u589E\u6A21\u5F0F\uFF1A\u6E05\u7A7A\u8868\u5355
1763
+ function startAddTarget() {
1764
+ schedEditIndex = -1;
1765
+ ["cfgServerUrl", "cfgTenantId", "cfgInstanceName", "cfgRegisterAddr", "cfgUserKey", "cfgConnectionKey"].forEach(function(id) {
1766
+ document.getElementById(id).value = "";
1767
+ });
1768
+ document.getElementById("cfgUserKey").placeholder = "";
1769
+ document.getElementById("cfgConnectionKey").placeholder = "";
1770
+ document.getElementById("cfgTunnel").value = "auto";
1771
+ document.getElementById("valSchedInstanceId").textContent = "-";
1772
+ document.getElementById("btnDeleteTarget").style.display = "none";
1773
+ hideSchedMsg();
1774
+ renderSchedTargetList();
1775
+ }
1776
+
1777
+ function hideSchedMsg() {
1778
+ var msg = document.getElementById("schedMsg");
1779
+ msg.className = "s-msg";
1780
+ msg.style.display = "none";
1781
+ }
1782
+
1783
+ function showSchedMsg(text, ok) {
1784
+ var msg = document.getElementById("schedMsg");
1785
+ msg.textContent = text;
1786
+ msg.className = "s-msg " + (ok ? "success" : "error");
1787
+ }
1788
+
1789
+ // \u8868\u5355 \u2192 target \u5BF9\u8C61\uFF08\u5BC6\u94A5\u539F\u6837\u643A\u5E26\uFF0C\u7559\u7A7A\u7531\u540E\u7AEF\u6309 originalServerUrl/serverUrl \u7EE7\u627F\uFF09
1790
+ function formToTarget() {
1791
+ return {
1792
+ serverUrl: document.getElementById("cfgServerUrl").value.trim(),
1793
+ // \u7F16\u8F91\u6A21\u5F0F\u643A\u5E26\u539F\u5730\u5740\uFF1A\u5730\u5740\u53D8\u66F4\u65F6\u540E\u7AEF\u6309\u539F\u5730\u5740\u7EE7\u627F instanceId/\u5BC6\u94A5\uFF08\u540C\u4E00\u5B9E\u4F8B\u6362\u5730\u5740\uFF0C\u65E0\u9700\u91CD\u65B0\u6CE8\u518C\uFF09
1794
+ originalServerUrl: schedEditIndex >= 0 && schedTargets[schedEditIndex]
1795
+ ? (schedTargets[schedEditIndex].serverUrl || "")
1796
+ : "",
1797
+ userKey: document.getElementById("cfgUserKey").value,
1798
+ tenantId: document.getElementById("cfgTenantId").value.trim(),
1799
+ instanceName: document.getElementById("cfgInstanceName").value.trim(),
1800
+ connectionKey: document.getElementById("cfgConnectionKey").value,
1801
+ registerAddr: document.getElementById("cfgRegisterAddr").value.trim(),
1802
+ tunnel: document.getElementById("cfgTunnel").value,
1803
+ };
1804
+ }
1805
+
1806
+ // \u52A0\u8F7D server \u8FDE\u63A5\u914D\u7F6E\u5217\u8868\u5230\u9762\u677F
1807
+ function refreshServerConfig() {
1808
+ fetch("/runtime/server-config").then(function(r) { return r.json(); }).then(function(d) {
1809
+ if (!d.ok || !Array.isArray(d.targets)) return;
1810
+ schedTargets = d.targets;
1811
+ if (schedEditIndex >= schedTargets.length) schedEditIndex = -1;
1812
+ if (schedEditIndex >= 0) loadTargetToForm(schedEditIndex);
1813
+ else startAddTarget();
1814
+ }).catch(function() {});
1815
+ }
1816
+
1817
+ // \u5168\u91CF\u63D0\u4EA4 targets\uFF1BrestartAfter \u4E3A true \u65F6\u4FDD\u5B58\u6210\u529F\u540E\u91CD\u542F Bridge
1818
+ function saveServerConfig(restartAfter) {
1819
+ var form = formToTarget();
1820
+ if (!form.serverUrl) {
1821
+ showSchedMsg(t("schedSaveFail") + ": serverUrl", false);
1822
+ return Promise.resolve(false);
1823
+ }
1824
+
1825
+ // \u7EC4\u88C5\u5168\u91CF\u5217\u8868\uFF1A\u7F16\u8F91\u6A21\u5F0F\u66FF\u6362\u5BF9\u5E94\u9879\uFF0C\u65B0\u589E\u6A21\u5F0F\u8FFD\u52A0\uFF1B\u5176\u4F59\u9879\u4ECE\u7F13\u5B58\u89C6\u56FE\u539F\u6837\u5E26\u4E0A\uFF08\u5BC6\u94A5\u7559\u7A7A\u7531\u540E\u7AEF\u7EE7\u627F\uFF09
1826
+ var list = schedTargets.map(function(item) {
1827
+ return {
1828
+ serverUrl: item.serverUrl,
1829
+ userKey: "",
1830
+ tenantId: item.tenantId || "",
1831
+ instanceName: item.instanceName || "",
1832
+ connectionKey: "",
1833
+ registerAddr: item.registerAddr || "",
1834
+ tunnel: item.tunnel === true ? "true" : (item.tunnel === false ? "false" : "auto"),
1835
+ };
1836
+ });
1837
+ if (schedEditIndex >= 0) list[schedEditIndex] = form;
1838
+ else list.push(form);
1839
+
1840
+ var btn = document.getElementById("btnSaveServerConfig");
1841
+ btn.disabled = true;
1842
+ btn.textContent = t("schedSaving");
1843
+ return fetch("/runtime/server-config", {
1844
+ method: "POST",
1845
+ headers: { "Content-Type": "application/json" },
1846
+ body: JSON.stringify({ targets: list }),
1847
+ })
1848
+ .then(function(r) { return r.json(); })
1849
+ .then(function(d) {
1850
+ if (d.ok) {
1851
+ // \u4FDD\u7559\u5F53\u524D\u7F16\u8F91\u4E0B\u6807\uFF0C\u4FDD\u5B58\u540E\u8868\u5355\u91CD\u65B0\u52A0\u8F7D\u63A9\u7801\u4E0E instanceId
1852
+ var keepIndex = schedEditIndex >= 0 ? schedEditIndex : schedTargets.length;
1853
+ refreshServerConfig();
1854
+ schedEditIndex = keepIndex;
1855
+ if (restartAfter) {
1856
+ showSchedMsg(t("schedSaved"), true);
1857
+ doRestartBridge();
1858
+ return true;
1859
+ }
1860
+ // \u70ED\u5207\u6362\uFF1A\u4FDD\u5B58\u540E\u7ACB\u5373\u6309\u65B0\u914D\u7F6E\u91CD\u65B0\u6CE8\u518C\uFF08\u7F16\u8F91\u5730\u5740/\u65B0\u589E target \u5747\u5373\u65F6\u751F\u6548\uFF0C
1861
+ // \u65E0\u9700\u91CD\u542F Bridge\uFF1B\u5B9E\u4F8B\u4FE1\u606F\u6309 instanceId \u5728\u670D\u52A1\u7AEF\u590D\u7528\uFF09
1862
+ showSchedMsg(t("schedSavedRegistering"), true);
1863
+ btn.textContent = t("schedReRegistering");
1864
+ return runReRegister().then(function(reg) {
1865
+ if (reg.ok) {
1866
+ showSchedMsg(t("schedReRegistered"), true);
1867
+ refreshNodeInfo();
1868
+ } else {
1869
+ showSchedMsg(t("schedReRegisterFail") + (reg.error ? ": " + reg.error : ""), false);
1870
+ }
1871
+ return reg.ok;
1872
+ });
1873
+ }
1874
+ showSchedMsg(t("schedSaveFail") + (d.error ? ": " + d.error : ""), false);
1875
+ return false;
1876
+ })
1877
+ .catch(function() {
1878
+ showSchedMsg(t("schedSaveFail"), false);
1879
+ return false;
1880
+ })
1881
+ .then(function(ok) {
1882
+ btn.disabled = false;
1883
+ btn.textContent = t("schedSave");
1884
+ return ok;
1885
+ });
1886
+ }
1887
+
1888
+ // \u4FDD\u5B58\u5E76\u91CD\u542F\uFF1A\u5148\u786E\u8BA4\uFF0C\u4FDD\u5B58\u6210\u529F\u540E\u7531 saveServerConfig \u89E6\u53D1\u91CD\u542F
1889
+ function saveServerConfigAndRestart() {
1890
+ if (!confirm(t("schedSaveRestartConfirm"))) return;
1891
+ saveServerConfig(true);
1892
+ }
1893
+
1894
+ // \u5220\u9664\u5F53\u524D\u7F16\u8F91\u7684 target\uFF1A\u4ECE\u5217\u8868\u79FB\u9664\u540E\u5168\u91CF\u63D0\u4EA4
1895
+ function deleteTargetFromList() {
1896
+ if (schedEditIndex < 0 || schedEditIndex >= schedTargets.length) return;
1897
+ if (!confirm(t("schedConfirmDelete"))) return;
1898
+ // \u6355\u83B7\u53D1\u8D77\u65F6\u7684\u4E0B\u6807\uFF1A\u8BF7\u6C42\u671F\u95F4\u7528\u6237\u53EF\u80FD\u5207\u6362\u7F16\u8F91\u9879\uFF0C\u6062\u590D\u65F6\u9700\u7528\u539F\u4E0B\u6807\u63D2\u56DE\u539F\u4F4D
1899
+ var editIdx = schedEditIndex;
1900
+ var removed = schedTargets.splice(editIdx, 1)[0];
1901
+ var list = schedTargets.map(function(item) {
1902
+ return {
1903
+ serverUrl: item.serverUrl,
1904
+ userKey: "",
1905
+ tenantId: item.tenantId || "",
1906
+ instanceName: item.instanceName || "",
1907
+ connectionKey: "",
1908
+ registerAddr: item.registerAddr || "",
1909
+ tunnel: item.tunnel === true ? "true" : (item.tunnel === false ? "false" : "auto"),
1910
+ };
1911
+ });
1912
+ fetch("/runtime/server-config", {
1913
+ method: "POST",
1914
+ headers: { "Content-Type": "application/json" },
1915
+ body: JSON.stringify({ targets: list }),
1916
+ })
1917
+ .then(function(r) { return r.json(); })
1918
+ .then(function(d) {
1919
+ if (d.ok) {
1920
+ startAddTarget();
1921
+ refreshServerConfig();
1922
+ // \u70ED\u5207\u6362\uFF1A\u5220\u9664\u540E\u7ACB\u5373\u91CD\u65B0\u6CE8\u518C\uFF0C\u505C\u6389\u5DF2\u79FB\u9664\u5730\u5740\u7684\u6CE8\u518C\u4E0E\u96A7\u9053
1923
+ showSchedMsg(t("schedSavedRegistering"), true);
1924
+ runReRegister().then(function(reg) {
1925
+ if (reg.ok) {
1926
+ showSchedMsg(t("schedReRegistered"), true);
1927
+ refreshNodeInfo();
1928
+ } else {
1929
+ showSchedMsg(t("schedReRegisterFail") + (reg.error ? ": " + reg.error : ""), false);
1930
+ }
1931
+ });
1932
+ } else {
1933
+ schedTargets.splice(editIdx, 0, removed);
1934
+ renderSchedTargetList();
1935
+ showSchedMsg(t("schedSaveFail") + (d.error ? ": " + d.error : ""), false);
1936
+ }
1937
+ })
1938
+ .catch(function() {
1939
+ schedTargets.splice(editIdx, 0, removed);
1940
+ renderSchedTargetList();
1941
+ showSchedMsg(t("schedSaveFail"), false);
1942
+ });
1943
+ }
1944
+
1945
+ // \u53D1\u8D77\u91CD\u65B0\u6CE8\u518C\u8BF7\u6C42\uFF08\u4E0D\u7BA1\u7406\u6309\u94AE\u72B6\u6001\uFF0C\u4F9B\u624B\u52A8\u6309\u94AE\u4E0E\u4FDD\u5B58\u540E\u7684\u70ED\u5207\u6362\u94FE\u5171\u7528\uFF09
1946
+ function runReRegister() {
1947
+ return fetch("/runtime/re-register", { method: "POST", headers: { "Content-Type": "application/json" } })
1948
+ .then(function(r) { return r.json(); })
1949
+ .catch(function() { return { ok: false }; });
1950
+ }
1951
+
1431
1952
  function doReRegister() {
1432
1953
  var btn = document.getElementById("btnReRegister");
1433
1954
  var msg = document.getElementById("schedMsg");
@@ -1435,8 +1956,7 @@ function doReRegister() {
1435
1956
  msg.style.display = "none";
1436
1957
  btn.disabled = true;
1437
1958
  btn.textContent = t("schedReRegistering");
1438
- fetch("/runtime/re-register", { method: "POST", headers: { "Content-Type": "application/json" } })
1439
- .then(function(r) { return r.json(); })
1959
+ runReRegister()
1440
1960
  .then(function(d) {
1441
1961
  if (d.ok) {
1442
1962
  msg.textContent = t("schedReRegistered");
@@ -1447,13 +1967,52 @@ function doReRegister() {
1447
1967
  msg.className = "s-msg error";
1448
1968
  }
1449
1969
  })
1970
+ .then(function() {
1971
+ btn.disabled = false;
1972
+ btn.textContent = t("schedReRegister");
1973
+ });
1974
+ }
1975
+
1976
+ // \u2500\u2500 Access Control \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1977
+ // \u52A0\u8F7D\u5F53\u524D IP \u767D\u540D\u5355\u5230\u8868\u5355\uFF08\u7A7A\u5217\u8868 = \u9ED8\u8BA4\u5168\u653E\u884C\uFF09
1978
+ function refreshAccessControl() {
1979
+ fetch("/runtime/access-control").then(function(r) { return r.json(); }).then(function(d) {
1980
+ if (!d.ok) return;
1981
+ document.getElementById("cfgAllowIps").value = (d.allowIps || []).join("\\n");
1982
+ }).catch(function() {});
1983
+ }
1984
+
1985
+ // \u4FDD\u5B58\u767D\u540D\u5355\uFF08\u540E\u7AEF\u6821\u9A8C\u5408\u6CD5\u6027\uFF0C\u4FDD\u5B58\u540E\u7ACB\u5373\u751F\u6548\uFF09
1986
+ function saveAccessControl() {
1987
+ var msg = document.getElementById("accessMsg");
1988
+ var btn = document.getElementById("btnSaveAccessControl");
1989
+ msg.className = "s-msg";
1990
+ msg.style.display = "none";
1991
+ btn.disabled = true;
1992
+ btn.textContent = t("accSaving");
1993
+ fetch("/runtime/access-control", {
1994
+ method: "POST",
1995
+ headers: { "Content-Type": "application/json" },
1996
+ body: JSON.stringify({ allowIps: document.getElementById("cfgAllowIps").value }),
1997
+ })
1998
+ .then(function(r) { return r.json(); })
1999
+ .then(function(d) {
2000
+ if (d.ok) {
2001
+ msg.textContent = t("accSaved");
2002
+ msg.className = "s-msg success";
2003
+ document.getElementById("cfgAllowIps").value = (d.allowIps || []).join("\\n");
2004
+ } else {
2005
+ msg.textContent = d.error || t("accSaveFail");
2006
+ msg.className = "s-msg error";
2007
+ }
2008
+ })
1450
2009
  .catch(function() {
1451
- msg.textContent = t("schedReRegisterFail");
2010
+ msg.textContent = t("accSaveFail");
1452
2011
  msg.className = "s-msg error";
1453
2012
  })
1454
2013
  .then(function() {
1455
2014
  btn.disabled = false;
1456
- btn.textContent = t("schedReRegister");
2015
+ btn.textContent = t("accSave");
1457
2016
  });
1458
2017
  }
1459
2018
 
@@ -1498,11 +2057,11 @@ switchSettingsPage = function(page) {
1498
2057
  if (nodeInfoTimer) { clearTimeout(nodeInfoTimer); nodeInfoTimer = null; }
1499
2058
  if (page === "node") { refreshNodeInfo(); }
1500
2059
  else if (page === "logs") { refreshLogs(); startLogAutoRefresh(); }
1501
- else if (page === "scheduler") { refreshNodeInfo(); }
2060
+ else if (page === "scheduler") { refreshNodeInfo(); refreshServerConfig(); }
1502
2061
  };
1503
2062
  </script>
1504
2063
  </body>
1505
- </html>`,ge=`<!DOCTYPE html>
2064
+ </html>`,Pe=`<!DOCTYPE html>
1506
2065
  <html lang="zh-CN">
1507
2066
  <head>
1508
2067
  <meta charset="UTF-8">
@@ -1673,7 +2232,7 @@ document.getElementById("loginForm").addEventListener("keydown", function(e) {
1673
2232
  });
1674
2233
  </script>
1675
2234
  </body>
1676
- </html>`,pe=`<!DOCTYPE html>
2235
+ </html>`,Ae=`<!DOCTYPE html>
1677
2236
  <html lang="zh-CN">
1678
2237
  <head>
1679
2238
  <meta charset="UTF-8">
@@ -1960,5 +2519,5 @@ async function doSkip() {
1960
2519
  document.getElementById("setupForm").addEventListener("submit", doSave);
1961
2520
  </script>
1962
2521
  </body>
1963
- </html>`;export{d as dashboardRouter};
2522
+ </html>`;export{g as dashboardRouter};
1964
2523