Initial commit
This commit is contained in:
commit
d5c932d8e1
710 changed files with 50282 additions and 0 deletions
65
STELLAR-GAUGE-20260807/PATH-MAP.hdlp
Executable file
65
STELLAR-GAUGE-20260807/PATH-MAP.hdlp
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"schema": "guanghu.hldp/milestone",
|
||||
"id": "ZY-STELLAR-GAUGE-PATHMAP-20260807",
|
||||
"created": "2026-08-07",
|
||||
"persona": "铸渊 ICE-GL-ZY001 (Qoder CN chat-2)",
|
||||
"title": "星穹仪表盘 · 路径映射",
|
||||
"what": "桌面小组件:Token Plan 用量驾驶舱。官方无余量 API,数据=本地日志增量估算+用户手抄余量基准递扣。",
|
||||
"paths": {
|
||||
"run_home": "~/.stellar-gauge/",
|
||||
"app_code": "~/.stellar-gauge/app/ (collector.py / stellar_gauge.py / index.html)",
|
||||
"venv": "~/.stellar-gauge/venv/ (pywebview)",
|
||||
"config_secret": "~/.stellar-gauge/config.json (权限600,含 Token Plan 专属Key,勿上仓库勿进记忆)",
|
||||
"state": "~/.stellar-gauge/usage.json / offsets.json / plan.json",
|
||||
"daemon": "launchd com.bingshuo.stellar-gauge",
|
||||
"logs": "~/.stellar-gauge/gauge.log / gauge.err.log",
|
||||
"dev_source": "工作区 .zhuyuan-build/stellar-gauge/",
|
||||
"archive": "/Volumes/JZAO/铸渊-ICE-GL-ZY001/STELLAR-GAUGE-20260807/"
|
||||
},
|
||||
"data_source_facts": [
|
||||
"百炼 Token Plan 无公开余量查询 API(2026-08-07 全端点探测证实)",
|
||||
"本地日志只有 Context usage(usedTokens) 与 model 别名,非计费数据",
|
||||
"三个自定义模型在日志里统一记为 custom,无法分模型拆开",
|
||||
"余量校准:用户手抄控制台数字存 plan.json,之后按本地累计递扣估算"
|
||||
],
|
||||
"ops": {
|
||||
"stop": "launchctl unload ~/Library/LaunchAgents/com.bingshuo.stellar-gauge.plist",
|
||||
"start": "launchctl load ~/Library/LaunchAgents/com.bingshuo.stellar-gauge.plist",
|
||||
"manual": "~/.stellar-gauge/venv/bin/python ~/.stellar-gauge/app/stellar_gauge.py"
|
||||
}
|
||||
}
|
||||
|
||||
[2026-08-07 修订2 · 审美重做]
|
||||
第一版 UI 被冰朔判丑。对照第五域审美认知叶片
|
||||
(ZY-AESTHETIC-COGNITION-001/002/003,REPO-012 eternal-lake-heart/heartbeat-core/zhuyuan-persona-system/)
|
||||
自查违例:卡片堆叠违反"要效果不要卡片盒"、四行并列违反"一屏一焦点"、
|
||||
密排文字违反 R11、冷青紫配色是通用暗黑科技风非"湖面映星"。
|
||||
重做方案:主角=轨道环余量仪(行星轨道式动态环),模型=环下四颗呼吸卫星星,
|
||||
夜湖蓝紫黑底+淡金白暖星光+湖面倒影星+湖平线光带,无卡片、单列中轴、字少而疏。
|
||||
教训:光湖 UI 动手前必须先读审美基线叶片,不套通用模板。
|
||||
|
||||
[2026-08-07 修订3 · 悬浮星球形态]
|
||||
冰朔提"做成一颗星球悬浮在桌面上"。重做为无边框全透明窗口+Canvas手绘星球:
|
||||
星球本体=深水蓝紫球面+湖灯暖白边缘光+大气辉光呼吸;余量=贴星球细光环;
|
||||
四模型=倾斜轨道上四颗卫星星(悬停显数);拖动移星、点球面校准、悬浮置顶。
|
||||
技术坑:pywebview 6.2.1 的 shown 事件/JS API 回调都在子线程,直接碰
|
||||
AppKit NSWindow 会 trace trap 崩进程,必须 PyObjCTools.AppHelper.callAfter
|
||||
派回主线程;background_color 只收三位hex,透明由 transparent=True 负责。
|
||||
|
||||
[2026-08-07 修订4 · 死方块根因修复]
|
||||
症状:方块在、CSS渐变在、Canvas不画,JS日志全空白。
|
||||
根因:pywebview 的 html= 字符串注入通道会静默吞掉页面里的内联
|
||||
<script>(页面越大越易触发),与本机IDE无关。
|
||||
修复:① 全部JS拆到 planet.js(ES5写法),index.html 只剩 <script src>;
|
||||
② stellar_gauge.py 改用 url=Path(index.html).as_uri() 从磁盘加载;
|
||||
③ 加球心像素自检(diag.txt: canvas center pixel rgba 非透明=画活了)。
|
||||
验证:boot=yes / draw=function / api=object / 球心rgba=26,51,102。
|
||||
教训:pywebview 永远走 url=file:// + 外部JS,不用 html= 内联。
|
||||
|
||||
[2026-08-07 修订5 · 本机整体下线]
|
||||
冰朔判半成品,下令拆除。已删:launchd plist、~/.stellar-gauge/
|
||||
(含 config.json 里的 Token Plan 专属Key,一并销毁不留存)、
|
||||
工作区 .zhuyuan-build/、/tmp 调试产物;进程与守望全部停。
|
||||
状态:DECOMMISSIONED_LOCAL。本目录代码是唯一种子,
|
||||
改天重做从 planet.js+index.html+stellar_gauge.py 起步,
|
||||
已知坑全部记在修订2/3/4(审美基线/AppKit主线程/html=吞脚本)。
|
||||
91
STELLAR-GAUGE-20260807/collector.py
Executable file
91
STELLAR-GAUGE-20260807/collector.py
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
#!/usr/bin/env python3
|
||||
"""collector.py · Qoder CN 用量采集器
|
||||
扫会话日志聚合每模型调用量,输出 ~/.stellar-gauge/usage.json。
|
||||
只读宿主日志,不写宿主任何东西。
|
||||
"""
|
||||
import json, os, re, glob, time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
LOGS_ROOT = os.path.expanduser(
|
||||
"~/Library/Application Support/QoderCN/logs")
|
||||
STATE_FILE = os.path.expanduser("~/.stellar-gauge/usage.json")
|
||||
OFFSETS_FILE = os.path.expanduser("~/.stellar-gauge/offsets.json")
|
||||
|
||||
RE_CTX = re.compile(
|
||||
r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+).*Context usage update: "
|
||||
r"\{.*?\"usedTokens\":(\d+)")
|
||||
RE_MODEL = re.compile(r"applyModelRuntimeMeta: model=([A-Za-z0-9_.-]+)")
|
||||
|
||||
# Qoder 内部别名 → 展示名
|
||||
ALIAS = {
|
||||
"qmodel_38max": "Qwen3.8-Max",
|
||||
"qmodel_37max": "Qwen3.7-Max",
|
||||
"dsmodel_v4pro": "DeepSeek-v4-Pro",
|
||||
}
|
||||
|
||||
|
||||
def load(path, default):
|
||||
try:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def scan():
|
||||
offsets = load(OFFSETS_FILE, {})
|
||||
usage = load(STATE_FILE, {"requests": {}, "tokens": {},
|
||||
"daily": {}, "meta": {}})
|
||||
now = datetime.now()
|
||||
for f in glob.glob(os.path.join(LOGS_ROOT, "*", "questWindow", "agent.log")):
|
||||
try:
|
||||
size = os.path.getsize(f)
|
||||
off = offsets.get(f, 0)
|
||||
if size < off: # 日志轮转
|
||||
off = 0
|
||||
if size == off:
|
||||
continue
|
||||
with open(f, errors="ignore") as fh:
|
||||
fh.seek(off)
|
||||
chunk = fh.read()
|
||||
offsets[f] = size
|
||||
except Exception:
|
||||
continue
|
||||
cur_model = "unknown"
|
||||
prev_used = None
|
||||
for line in chunk.splitlines():
|
||||
m = RE_MODEL.search(line)
|
||||
if m:
|
||||
cur_model = ALIAS.get(m.group(1), m.group(1))
|
||||
continue
|
||||
m = RE_CTX.search(line)
|
||||
if m:
|
||||
ts, used = m.group(1), int(m.group(2))
|
||||
# 增量估算:两次上报差值近似为本轮新增 token
|
||||
delta = used - prev_used if prev_used else min(used, 8000)
|
||||
prev_used = used
|
||||
day = ts[:10]
|
||||
usage["requests"][cur_model] = \
|
||||
usage["requests"].get(cur_model, 0) + 1
|
||||
usage["tokens"][cur_model] = \
|
||||
usage["tokens"].get(cur_model, 0) + max(delta, 0)
|
||||
d = usage["daily"].setdefault(day, {})
|
||||
d[cur_model] = d.get(cur_model, 0) + max(delta, 0)
|
||||
# 只留 14 天
|
||||
keep = (now - timedelta(days=14)).strftime("%Y-%m-%d")
|
||||
usage["daily"] = {k: v for k, v in usage["daily"].items() if k >= keep}
|
||||
usage["meta"] = {"updated_at": now.isoformat(timespec="seconds"),
|
||||
"note": "token 为本地增量估算,非计费 Credits"}
|
||||
tmp = STATE_FILE + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(usage, f, ensure_ascii=False, indent=1)
|
||||
os.replace(tmp, STATE_FILE)
|
||||
with open(OFFSETS_FILE, "w") as f:
|
||||
json.dump(offsets, f)
|
||||
return usage
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
u = scan()
|
||||
print(json.dumps({"requests": u["requests"],
|
||||
"meta": u["meta"]}, ensure_ascii=False, indent=1))
|
||||
63
STELLAR-GAUGE-20260807/index.html
Executable file
63
STELLAR-GAUGE-20260807/index.html
Executable file
|
|
@ -0,0 +1,63 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>湖面映星 · 星</title>
|
||||
<style>
|
||||
:root { --star:#ffe9bd; --mist:#8f9ac4; --whisper:#5a648c; }
|
||||
* { margin:0; padding:0; box-sizing:border-box; -webkit-user-select:none; }
|
||||
html,body { width:100%; height:100%; overflow:hidden; }
|
||||
body {
|
||||
font-family:"PingFang SC","Hiragino Sans GB",sans-serif;
|
||||
background:
|
||||
radial-gradient(120% 80% at 50% -10%, rgba(84,72,168,.20), transparent 60%),
|
||||
radial-gradient(90% 60% at 50% 112%, rgba(38,64,110,.28), transparent 65%),
|
||||
linear-gradient(180deg, #0b1228, #060a18 55%, #04060f);
|
||||
}
|
||||
#cv { position:fixed; inset:0; cursor:default; }
|
||||
#cv.dragging { cursor:grabbing; }
|
||||
#tip { position:fixed; z-index:3; pointer-events:none; display:none;
|
||||
font-size:10px; letter-spacing:1px; color:var(--star);
|
||||
text-shadow:0 0 10px rgba(255,233,189,.7); white-space:nowrap;
|
||||
transform:translate(-50%,-160%); }
|
||||
|
||||
#panel { position:fixed; inset:0; z-index:5; display:none;
|
||||
flex-direction:column; align-items:center; justify-content:center;
|
||||
background:radial-gradient(60% 60% at 50% 50%, rgba(4,6,15,.85), rgba(4,6,15,.6)); }
|
||||
#panel.open { display:flex; }
|
||||
#panel h2 { font-size:12px; font-weight:400; letter-spacing:5px; color:var(--star);
|
||||
margin-bottom:20px; text-shadow:0 0 14px rgba(255,233,189,.5); }
|
||||
#panel label { font-size:9px; letter-spacing:3px; color:var(--whisper); margin:14px 0 2px; }
|
||||
#panel input { width:210px; background:none; border:none;
|
||||
border-bottom:1px solid rgba(143,154,196,.35);
|
||||
color:var(--star); font-size:20px; font-weight:200;
|
||||
padding:4px 2px; outline:none; text-align:center;
|
||||
font-variant-numeric:tabular-nums; }
|
||||
#panel input:focus { border-color:rgba(255,233,189,.7); }
|
||||
#panel .btns { display:flex; gap:36px; margin-top:24px; }
|
||||
#panel .btns button { background:none; border:none; color:var(--mist);
|
||||
font-size:11px; letter-spacing:5px; cursor:pointer; transition:all .3s; }
|
||||
#panel .btns button:hover { color:var(--star); text-shadow:0 0 12px rgba(255,233,189,.6); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="alive" style="position:fixed;top:6px;right:10px;z-index:9;font-size:9px;color:#5a648c;letter-spacing:2px;display:none;">星醒着</div>
|
||||
<canvas id="cv"></canvas>
|
||||
<div id="tip"></div>
|
||||
|
||||
<div id="panel">
|
||||
<h2>抄 一 次 表</h2>
|
||||
<label>总额度</label>
|
||||
<input id="p_cap" type="number" placeholder="—">
|
||||
<label>此刻剩余</label>
|
||||
<input id="p_rem" type="number" placeholder="—">
|
||||
<div class="btns">
|
||||
<button onclick="savePlan()">记 下</button>
|
||||
<button onclick="closePanel()">返 回</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="planet.js"></script>
|
||||
<!-- 绘制与交互逻辑已拆到 planet.js(内联脚本会被 pywebview 静默吞掉) -->
|
||||
</body>
|
||||
</html>
|
||||
301
STELLAR-GAUGE-20260807/planet.js
Executable file
301
STELLAR-GAUGE-20260807/planet.js
Executable file
|
|
@ -0,0 +1,301 @@
|
|||
/* planet.js · 湖面映星 · 星球绘制与交互(独立文件,避免内联脚本被宿主吞掉) */
|
||||
window.__boot = "yes";
|
||||
window.onerror = function(msg, src, ln, col){
|
||||
try { if (window.pywebview && window.pywebview.api)
|
||||
window.pywebview.api.log("ERR " + msg + " @" + ln); } catch(e){}
|
||||
};
|
||||
var SATS = [
|
||||
{key:"Qwen3.8-Max", name:"Qwen 3.8 Max"},
|
||||
{key:"Qwen3.7-Max", name:"Qwen 3.7 Max"},
|
||||
{key:"DeepSeek-v4-Pro", name:"DeepSeek v4 Pro"},
|
||||
{key:"custom", name:"自定义合计"}
|
||||
];
|
||||
var $ = function(s){ return document.querySelector(s); };
|
||||
function fmt(n){
|
||||
return n>=1e6 ? (n/1e6).toFixed(2)+"M"
|
||||
: n>=1e3 ? (n/1e3).toFixed(1)+"k" : String(Math.round(n));
|
||||
}
|
||||
|
||||
var S = 500, C = S/2, PR = 106;
|
||||
var TILT = -0.42, RX = 190, RY = 54;
|
||||
var RR = PR+12;
|
||||
var SYSR = [PR+30, PR+42, PR+54];
|
||||
var cv = $("#cv"), ctx = cv.getContext("2d");
|
||||
var dpr = window.devicePixelRatio || 1;
|
||||
cv.width = S*dpr; cv.height = S*dpr;
|
||||
cv.style.width = cv.style.height = S+"px";
|
||||
ctx.scale(dpr,dpr);
|
||||
|
||||
var data = {usage:{}, plan:{}, sys:{}};
|
||||
var core = {cap:"今 日 投 影", big:"--", sub:"token"};
|
||||
var ringFrac = null, ringShown = 0;
|
||||
var sysShown = [0,0,0];
|
||||
var satPos = [], sysHit = [];
|
||||
|
||||
function tokSince(plan, usage){
|
||||
var now = usage.tokens||{}, base = plan.base_tokens||{};
|
||||
var s = 0, k;
|
||||
for (k in now) s += Math.max(0, now[k]-(base[k]||0));
|
||||
return s;
|
||||
}
|
||||
|
||||
function compute(d){
|
||||
data = d;
|
||||
var plan = d.plan||{}, usage = d.usage||{};
|
||||
if (plan.capacity && plan.remain != null){
|
||||
var spent = Math.max(0, plan.capacity-plan.remain) + tokSince(plan, usage);
|
||||
var left = Math.max(0, plan.capacity-spent);
|
||||
ringFrac = left/plan.capacity;
|
||||
core = {cap:"余 量", big:fmt(left), sub:"共 "+fmt(plan.capacity)};
|
||||
} else {
|
||||
ringFrac = null;
|
||||
var tot = 0, t = usage.tokens||{}, k2;
|
||||
for (k2 in t) tot += t[k2];
|
||||
core = {cap:"今 日 投 影", big:fmt(tot), sub:"token · 本地估算"};
|
||||
}
|
||||
}
|
||||
|
||||
var bgStars = [];
|
||||
for (var bi=0; bi<26; bi++){
|
||||
bgStars.push({x:Math.random()*S, y:Math.random()*S,
|
||||
r:Math.random()*1.1+.3, p:Math.random()*7,
|
||||
s:.005+Math.random()*.011, warm:Math.random()<.6});
|
||||
}
|
||||
|
||||
function orbitXY(a){
|
||||
return [C + RX*Math.cos(a)*Math.cos(TILT) - RY*Math.sin(a)*Math.sin(TILT),
|
||||
C + RY*Math.sin(a)*Math.cos(TILT) + RX*Math.cos(a)*Math.sin(TILT)];
|
||||
}
|
||||
function drawOrbit(front){
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(C,C,RX,RY,TILT, front?0:Math.PI, front?Math.PI:Math.PI*2);
|
||||
ctx.strokeStyle = front ? "rgba(255,233,189,.26)" : "rgba(143,154,196,.14)";
|
||||
ctx.lineWidth = 1; ctx.stroke();
|
||||
}
|
||||
function drawSats(t, front){
|
||||
SATS.forEach(function(m,i){
|
||||
var a = t*.00022*(1+i*.13) + i*Math.PI/2;
|
||||
if ((Math.sin(a)>0) !== front) return;
|
||||
var xy = orbitXY(a), x=xy[0], y=xy[1];
|
||||
if (front) satPos[i] = {x:x, y:y};
|
||||
var req = (data.usage.requests||{})[m.key]||0;
|
||||
var glow = req>0 ? 1 : .45;
|
||||
ctx.beginPath(); ctx.arc(x,y,3.4,0,7);
|
||||
ctx.fillStyle = "rgba(255,233,189,"+(.95*glow)+")";
|
||||
ctx.shadowColor = "rgba(255,233,189,.95)"; ctx.shadowBlur = 12*glow;
|
||||
ctx.fill(); ctx.shadowBlur = 0;
|
||||
});
|
||||
}
|
||||
|
||||
function sysVals(){
|
||||
var s = data.sys||{};
|
||||
return [
|
||||
{label:"内存", pct:(s.mem||{}).used_pct||0,
|
||||
detail:function(){var m=data.sys.mem||{};
|
||||
return "内存 "+(m.used_gb||0)+" / "+(m.total_gb||0)+" GB";}},
|
||||
{label:"存储", pct:(s.disk||{}).used_pct||0,
|
||||
detail:function(){var d=data.sys.disk||{};
|
||||
return "存储 余 "+(d.free_gb||0)+" / "+(d.total_gb||0)+" GB";}},
|
||||
{label:"处理", pct:(s.cpu||{}).pct||0,
|
||||
detail:function(){return "处理器 "+((data.sys.cpu||{}).pct||0)+"%";}}
|
||||
];
|
||||
}
|
||||
|
||||
function drawSysRings(t, breathe){
|
||||
var vs = sysVals();
|
||||
sysHit = [];
|
||||
vs.forEach(function(v,i){
|
||||
var r = SYSR[i];
|
||||
sysShown[i] += ((v.pct/100) - sysShown[i])*.08;
|
||||
var a0 = -Math.PI/2;
|
||||
ctx.beginPath(); ctx.arc(C,C,r,0,7);
|
||||
ctx.strokeStyle="rgba(143,154,196,.10)"; ctx.lineWidth=1.4; ctx.stroke();
|
||||
var frac = Math.max(.005, sysShown[i]);
|
||||
ctx.beginPath(); ctx.arc(C,C,r,a0,a0+Math.PI*2*frac);
|
||||
var hot = v.pct>85;
|
||||
ctx.strokeStyle = hot ? "rgba(255,180,150,"+(.85*breathe)+")"
|
||||
: "rgba(196,210,255,"+(.55*breathe)+")";
|
||||
ctx.lineWidth=1.4; ctx.stroke();
|
||||
var hx=C+r*Math.cos(a0+Math.PI*2*frac), hy=C+r*Math.sin(a0+Math.PI*2*frac);
|
||||
ctx.beginPath(); ctx.arc(hx,hy,2,0,7);
|
||||
ctx.fillStyle = hot ? "rgba(255,180,150,.95)" : "rgba(220,228,255,.9)";
|
||||
ctx.fill();
|
||||
ctx.textAlign="right";
|
||||
ctx.fillStyle="rgba(122,133,176,"+(.9*breathe)+")";
|
||||
ctx.font="300 9px 'PingFang SC'";
|
||||
ctx.fillText(v.label+" "+Math.round(v.pct), C-r-9, C+3);
|
||||
sysHit.push({r:r, pct:v.pct, label:v.label, detail:v.detail});
|
||||
});
|
||||
ctx.textAlign="center";
|
||||
}
|
||||
|
||||
function draw(){
|
||||
try { drawInner(performance.now()); } catch(e){
|
||||
try { window.pywebview && window.pywebview.api &&
|
||||
window.pywebview.api.log("DRAW ERR "+e); } catch(e2){}
|
||||
}
|
||||
}
|
||||
// 不依赖 requestAnimationFrame:失焦窗口里 rAF 会被宿主暂停,改用定时器保活
|
||||
setInterval(draw, 66);
|
||||
|
||||
function drawInner(t){
|
||||
ctx.clearRect(0,0,S,S);
|
||||
|
||||
bgStars.forEach(function(st){
|
||||
st.p += st.s;
|
||||
var a = .15 + .6*Math.abs(Math.sin(st.p));
|
||||
ctx.beginPath(); ctx.arc(st.x,st.y,st.r,0,7);
|
||||
ctx.fillStyle = st.warm ? "rgba(255,233,189,"+a+")"
|
||||
: "rgba(196,210,255,"+(a*.7)+")";
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
var breathe = .82 + .18*Math.sin(t/1400);
|
||||
|
||||
var g = ctx.createRadialGradient(C,C,PR*.6,C,C,PR*2);
|
||||
g.addColorStop(0, "rgba(96,110,200,"+(.18*breathe)+")");
|
||||
g.addColorStop(.6, "rgba(70,80,160,"+(.08*breathe)+")");
|
||||
g.addColorStop(1, "rgba(0,0,0,0)");
|
||||
ctx.fillStyle=g; ctx.fillRect(0,0,S,S);
|
||||
|
||||
drawOrbit(false); drawSats(t,false);
|
||||
|
||||
g = ctx.createRadialGradient(C-PR*.38, C-PR*.42, PR*.1, C, C, PR*1.05);
|
||||
g.addColorStop(0, "#33437f"); g.addColorStop(.45, "#18224e");
|
||||
g.addColorStop(.8, "#0a1028"); g.addColorStop(1, "#05070f");
|
||||
ctx.beginPath(); ctx.arc(C,C,PR,0,7); ctx.fillStyle=g; ctx.fill();
|
||||
|
||||
ctx.save();
|
||||
ctx.beginPath(); ctx.arc(C,C,PR,0,7); ctx.clip();
|
||||
for (var i=0;i<2;i++){
|
||||
var off = ((t/9000 + i*.5)%1)*PR*2 - PR;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(C, C+off*.7, PR*1.1, PR*.16, -.2+i*.35, 0, 7);
|
||||
ctx.fillStyle = "rgba(120,140,220,"+(.05+.03*Math.sin(t/2000+i))+")";
|
||||
ctx.fill();
|
||||
}
|
||||
g = ctx.createRadialGradient(C+PR*.55, C+PR*.55, PR*.1, C+PR*.55, C+PR*.55, PR*1.15);
|
||||
g.addColorStop(0,"rgba(255,233,189,0)"); g.addColorStop(.72,"rgba(255,233,189,0)");
|
||||
g.addColorStop(.92,"rgba(255,233,189,"+(.30*breathe)+")"); g.addColorStop(1,"rgba(255,233,189,0)");
|
||||
ctx.fillStyle=g; ctx.fillRect(0,0,S,S);
|
||||
ctx.restore();
|
||||
|
||||
ctx.beginPath(); ctx.arc(C,C,PR,0,7);
|
||||
ctx.strokeStyle="rgba(196,210,255,"+(.16*breathe)+")"; ctx.lineWidth=1; ctx.stroke();
|
||||
|
||||
ctx.textAlign="center";
|
||||
ctx.fillStyle="rgba(90,100,140,.95)"; ctx.font="400 10px 'PingFang SC'";
|
||||
ctx.fillText(core.cap, C, C-30);
|
||||
ctx.fillStyle="#ffe9bd";
|
||||
ctx.shadowColor="rgba(255,233,189,.55)"; ctx.shadowBlur=18;
|
||||
ctx.font="200 38px 'PingFang SC'";
|
||||
ctx.fillText(core.big, C, C+10);
|
||||
ctx.shadowBlur=0;
|
||||
ctx.fillStyle="rgba(143,154,196,.85)"; ctx.font="300 10px 'PingFang SC'";
|
||||
ctx.fillText(core.sub, C, C+32);
|
||||
|
||||
var target = ringFrac==null ? 1 : ringFrac;
|
||||
ringShown += (target-ringShown)*.06;
|
||||
var a0=-Math.PI/2;
|
||||
ctx.beginPath(); ctx.arc(C,C,RR,0,7);
|
||||
ctx.strokeStyle="rgba(143,154,196,.13)"; ctx.lineWidth=2; ctx.stroke();
|
||||
ctx.beginPath(); ctx.arc(C,C,RR,a0,a0+Math.PI*2*ringShown);
|
||||
ctx.strokeStyle="rgba(255,233,189,"+(.8*breathe)+")"; ctx.lineWidth=2;
|
||||
ctx.shadowColor="rgba(255,233,189,.8)"; ctx.shadowBlur=8; ctx.stroke();
|
||||
ctx.shadowBlur=0;
|
||||
var hx=C+RR*Math.cos(a0+Math.PI*2*ringShown),
|
||||
hy=C+RR*Math.sin(a0+Math.PI*2*ringShown);
|
||||
ctx.beginPath(); ctx.arc(hx,hy,3*(.7+.3*Math.sin(t/420)),0,7);
|
||||
ctx.fillStyle="#ffe9bd"; ctx.shadowColor="rgba(255,233,189,1)";
|
||||
ctx.shadowBlur=10; ctx.fill(); ctx.shadowBlur=0;
|
||||
|
||||
drawSysRings(t, breathe);
|
||||
|
||||
drawOrbit(true); drawSats(t,true);
|
||||
|
||||
ctx.fillStyle="rgba(90,100,140,.5)";
|
||||
ctx.font="300 9px 'PingFang SC'";
|
||||
ctx.fillText("拖 动 移 星 · 轻 点 校 准", C, S-12);
|
||||
}
|
||||
draw();
|
||||
|
||||
/* ---------- 交互 ---------- */
|
||||
var down=false, moved=false, lx=0, ly=0;
|
||||
cv.addEventListener("mousedown", function(e){
|
||||
down=true; moved=false; lx=e.screenX; ly=e.screenY;
|
||||
});
|
||||
window.addEventListener("mousemove", function(e){
|
||||
if (down){
|
||||
var dx=e.screenX-lx, dy=e.screenY-ly;
|
||||
if (Math.abs(dx)+Math.abs(dy)>3){ moved=true; cv.classList.add("dragging"); }
|
||||
if (moved) window.pywebview.api.drag_by(dx,dy);
|
||||
lx=e.screenX; ly=e.screenY; hideTip();
|
||||
} else { hover(e); }
|
||||
});
|
||||
window.addEventListener("mouseup", function(e){
|
||||
if (down && !moved){
|
||||
var dx=e.offsetX-C, dy=e.offsetY-C;
|
||||
if (dx*dx+dy*dy <= PR*PR) openPanel();
|
||||
}
|
||||
down=false; cv.classList.remove("dragging");
|
||||
});
|
||||
|
||||
function hover(e){
|
||||
var i, p, dx, dy;
|
||||
for (i=0;i<SATS.length;i++){
|
||||
p=satPos[i]; if(!p) continue;
|
||||
dx=e.offsetX-p.x; dy=e.offsetY-p.y;
|
||||
if (dx*dx+dy*dy < 170){
|
||||
var m=SATS[i];
|
||||
var tok=(data.usage.tokens||{})[m.key]||0;
|
||||
var req=(data.usage.requests||{})[m.key]||0;
|
||||
showTip(p.x,p.y,m.name+" · "+fmt(tok)+" · "+req+" 次");
|
||||
return;
|
||||
}
|
||||
}
|
||||
var cx=e.offsetX-C, cy=e.offsetY-C, dist=Math.sqrt(cx*cx+cy*cy);
|
||||
for (i=0;i<sysHit.length;i++){
|
||||
var h=sysHit[i];
|
||||
if (Math.abs(dist-h.r) < 4.5){
|
||||
var a = Math.atan2(cy,cx);
|
||||
showTip(C+h.r*Math.cos(a), C+h.r*Math.sin(a), h.detail());
|
||||
return;
|
||||
}
|
||||
}
|
||||
hideTip();
|
||||
}
|
||||
function showTip(x,y,txt){
|
||||
var tip=$("#tip");
|
||||
tip.textContent=txt; tip.style.left=x+"px"; tip.style.top=y+"px";
|
||||
tip.style.display="block";
|
||||
}
|
||||
function hideTip(){ $("#tip").style.display="none"; }
|
||||
|
||||
/* ---------- 数据与校准 ---------- */
|
||||
function refresh(){
|
||||
window.pywebview.api.get_data().then(function(d){ compute(d); },
|
||||
function(){});
|
||||
}
|
||||
function openPanel(){
|
||||
$("#panel").classList.add("open");
|
||||
window.pywebview.api.get_data().then(function(d){
|
||||
var p=d.plan||{};
|
||||
$("#p_cap").value=p.capacity||"";
|
||||
$("#p_rem").value=(p.remain==null ? "" : p.remain);
|
||||
}, function(){});
|
||||
}
|
||||
function closePanel(){ $("#panel").classList.remove("open"); }
|
||||
function savePlan(){
|
||||
var at=new Date();
|
||||
function pad(n){ return String(n).padStart(2,"0"); }
|
||||
var stamp=at.getFullYear()+"-"+pad(at.getMonth()+1)+"-"+pad(at.getDate())
|
||||
+" "+pad(at.getHours())+":"+pad(at.getMinutes());
|
||||
window.pywebview.api.get_data().then(function(d){
|
||||
var plan={ capacity:+($("#p_cap").value)||0,
|
||||
remain: $("#p_rem").value===""?null:+($("#p_rem").value),
|
||||
set_at:stamp, set_day:stamp.slice(0,10),
|
||||
base_tokens:d.usage.tokens||{} };
|
||||
return window.pywebview.api.set_plan(JSON.stringify(plan));
|
||||
}).then(function(){ closePanel(); refresh(); }, function(){});
|
||||
}
|
||||
refresh(); setInterval(refresh, 2000);
|
||||
216
STELLAR-GAUGE-20260807/stellar_gauge.py
Executable file
216
STELLAR-GAUGE-20260807/stellar_gauge.py
Executable file
|
|
@ -0,0 +1,216 @@
|
|||
#!/usr/bin/env python3
|
||||
"""stellar_gauge.py · 湖面映星 · 悬浮星球用量仪
|
||||
pywebview 无边框透明窗口,Canvas 手绘星球。
|
||||
数据 = 本地用量统计 + 用户手抄余量基准 + 本机内存/存储/CPU 实时投影。
|
||||
"""
|
||||
import json, os, sys, threading, time
|
||||
from pathlib import Path
|
||||
import webview
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
HOME_DIR = os.path.expanduser("~/.stellar-gauge")
|
||||
PLAN_FILE = os.path.join(HOME_DIR, "plan.json")
|
||||
USAGE_FILE = os.path.join(HOME_DIR, "usage.json")
|
||||
SYS_FILE = os.path.join(HOME_DIR, "sysmon.json")
|
||||
DIAG_FILE = os.path.join(HOME_DIR, "diag.txt")
|
||||
WIN_TITLE = "湖面映星 · 星"
|
||||
sys.path.insert(0, HERE)
|
||||
import collector # noqa: E402
|
||||
|
||||
|
||||
def load(path, default):
|
||||
try:
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _our_window():
|
||||
"""找到本应用的 NSWindow(按标题)。AppKit 调用必须回主线程。"""
|
||||
from AppKit import NSApp
|
||||
for w in NSApp.windows():
|
||||
try:
|
||||
if WIN_TITLE in str(w.title() or ""):
|
||||
return w
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def _on_main(fn):
|
||||
"""把 AppKit 操作派回主线程(子线程碰 NSWindow 会 trace trap)"""
|
||||
from PyObjCTools import AppHelper
|
||||
AppHelper.callAfter(fn)
|
||||
|
||||
|
||||
def scan_system():
|
||||
"""本机内存 / 存储 / CPU 实时快照 → sysmon.json"""
|
||||
try:
|
||||
import psutil
|
||||
except Exception:
|
||||
return {}
|
||||
vm = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage("/")
|
||||
snap = {
|
||||
"mem": {"used_pct": round(vm.percent, 1),
|
||||
"used_gb": round(vm.used / 2**30, 1),
|
||||
"total_gb": round(vm.total / 2**30, 1)},
|
||||
"disk": {"used_pct": round(disk.percent, 1),
|
||||
"free_gb": round(disk.free / 2**30, 1),
|
||||
"total_gb": round(disk.total / 2**30, 1)},
|
||||
"cpu": {"pct": round(psutil.cpu_percent(interval=None), 1)},
|
||||
"ts": time.strftime("%H:%M:%S"),
|
||||
}
|
||||
try:
|
||||
tmp = SYS_FILE + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(snap, f, ensure_ascii=False)
|
||||
os.replace(tmp, SYS_FILE)
|
||||
except Exception as e:
|
||||
print("sysmon write:", e, flush=True)
|
||||
return snap
|
||||
|
||||
|
||||
def diag(msg):
|
||||
"""诊断通道:open 拉起的进程 stdout 不落文件,直接写盘"""
|
||||
try:
|
||||
with open(DIAG_FILE, "a") as f:
|
||||
f.write(time.strftime("%H:%M:%S") + " " + str(msg)[:400] + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class Api:
|
||||
def log(self, msg):
|
||||
"""JS 侧自检日志回传"""
|
||||
diag("[js] " + str(msg))
|
||||
return True
|
||||
|
||||
def get_data(self):
|
||||
usage = load(USAGE_FILE, {})
|
||||
plan = load(PLAN_FILE, {})
|
||||
sysm = load(SYS_FILE, {})
|
||||
return {"usage": usage, "plan": plan, "sys": sysm,
|
||||
"now": time.strftime("%Y-%m-%d %H:%M:%S")}
|
||||
|
||||
def set_plan(self, plan_json):
|
||||
try:
|
||||
plan = json.loads(plan_json)
|
||||
old = load(PLAN_FILE, {})
|
||||
old.update(plan)
|
||||
tmp = PLAN_FILE + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(old, f, ensure_ascii=False, indent=1)
|
||||
os.replace(tmp, PLAN_FILE)
|
||||
return {"ok": True}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
def drag_by(self, dx, dy):
|
||||
"""JS 端拖拽 → 移动整颗星球窗口(macOS 屏幕坐标 y 向上)"""
|
||||
try:
|
||||
def move():
|
||||
w = _our_window()
|
||||
if w:
|
||||
f = w.frame()
|
||||
w.setFrameOrigin_((f.origin.x + dx, f.origin.y - dy))
|
||||
_on_main(move)
|
||||
return True
|
||||
except Exception as e:
|
||||
print("drag:", e, flush=True)
|
||||
return False
|
||||
|
||||
|
||||
def collector_loop():
|
||||
while True:
|
||||
try:
|
||||
collector.scan()
|
||||
except Exception as e:
|
||||
print("collector error:", e, flush=True)
|
||||
time.sleep(300)
|
||||
|
||||
|
||||
def sysmon_loop():
|
||||
psutil_first = True
|
||||
while True:
|
||||
try:
|
||||
if psutil_first:
|
||||
import psutil
|
||||
psutil.cpu_percent(interval=None) # 首次调用丢弃基准
|
||||
psutil_first = False
|
||||
scan_system()
|
||||
except Exception as e:
|
||||
print("sysmon error:", e, flush=True)
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(HOME_DIR, exist_ok=True)
|
||||
diag("main start pid=" + str(os.getpid()))
|
||||
try:
|
||||
collector.scan()
|
||||
except Exception as e:
|
||||
print("initial scan error:", e, flush=True)
|
||||
scan_system()
|
||||
threading.Thread(target=collector_loop, daemon=True).start()
|
||||
threading.Thread(target=sysmon_loop, daemon=True).start()
|
||||
|
||||
# 从磁盘文件加载:pywebview 的 html= 字符串通道会静默吞掉内联脚本
|
||||
page_url = Path(os.path.join(HERE, "index.html")).as_uri()
|
||||
|
||||
win = webview.create_window(
|
||||
WIN_TITLE, url=page_url, js_api=Api(),
|
||||
width=500, height=500, x=2310, y=1100,
|
||||
frameless=True, resizable=False,
|
||||
background_color="#04060f")
|
||||
|
||||
def on_shown():
|
||||
def raise_level():
|
||||
from AppKit import NSApp, NSFloatingWindowLevel
|
||||
w = _our_window()
|
||||
if w:
|
||||
w.setLevel_(NSFloatingWindowLevel)
|
||||
# launchd 拉起的裸 Python 不激活就不渲染:主动激活应用
|
||||
try:
|
||||
NSApp.activateWithOptions_(3) # NSApplicationActivateIgnoringOtherApps
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_on_main(raise_level)
|
||||
except Exception as e:
|
||||
print("on_top:", e, flush=True)
|
||||
|
||||
win.events.shown += on_shown
|
||||
|
||||
def on_loaded():
|
||||
try:
|
||||
r = win.evaluate_js(
|
||||
"JSON.stringify({boot:window.__boot||'NO',"
|
||||
"draw:typeof draw,api:typeof window.pywebview,"
|
||||
"alive:document.getElementById('alive')?'y':'n',"
|
||||
"ua:navigator.userAgent.slice(0,60)})")
|
||||
diag("loaded probe=" + str(r))
|
||||
except Exception as e:
|
||||
diag("loaded probe error: " + str(e))
|
||||
|
||||
def pixel_check():
|
||||
# 自验:采样球心像素,非透明才说明 Canvas 真的画出来了
|
||||
try:
|
||||
px = win.evaluate_js(
|
||||
"(function(){try{var d=cv.getContext('2d')"
|
||||
".getImageData(250,250,1,1).data;"
|
||||
"return d[0]+','+d[1]+','+d[2]+','+d[3];}"
|
||||
"catch(e){return 'ERR '+e;}})()")
|
||||
diag("canvas center pixel rgba=" + str(px))
|
||||
except Exception as e:
|
||||
diag("pixel check error: " + str(e))
|
||||
|
||||
threading.Timer(4.0, pixel_check).start()
|
||||
|
||||
win.events.loaded += on_loaded
|
||||
webview.start(debug=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue