@walkhi/code-relax 0.1.0-beta.21 → 0.1.0-beta.23
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.
- package/dist/bin/migrate-relay-db.mjs +4 -2
- package/dist/bin/self-relay-server.mjs +20 -19
- package/dist/shared/app-server-events.cjs +107 -100
- package/dist/shared/service-endpoints.cjs +21 -0
- package/dist/shared/timeline-sync.cjs +94 -5
- package/dist/skills/code-relax/SKILL.md +21 -18
- package/dist/src/lan-socket-server.mjs +50 -10
- package/dist/src/self-relay/admin-page.mjs +18 -11
- package/dist/src/self-relay/admin-server.mjs +1 -2
- package/dist/src/self-relay/connector.mjs +29 -4
- package/dist/src/self-relay/diagnostics.mjs +54 -0
- package/dist/src/self-relay/lifecycle.mjs +160 -160
- package/dist/src/self-relay/mysql-state.mjs +2 -1
- package/dist/src/self-relay/server.mjs +22 -23
- package/dist/src/self-relay/usage-daily.mjs +55 -0
- package/dist/src/self-relay/usage-meter.mjs +42 -31
- package/dist/src/server.mjs +100 -203
- package/dist/src/shared-app-server.mjs +132 -116
- package/dist/src/shared-thread-stream.mjs +56 -50
- package/dist/src/turn-model-history.mjs +47 -0
- package/package.json +1 -1
- package/dist/src/self-relay/admin-usage-page.mjs +0 -28
- package/dist/src/self-relay/usage-diagnostics.mjs +0 -46
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { adminUiTokens } from './admin-style.mjs';
|
|
2
|
-
|
|
3
|
-
export function adminUsagePage() {
|
|
4
|
-
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="only light"><title>使用统计 · Code Relax 管理</title><style>${adminUiTokens}</style><style>
|
|
5
|
-
:root{font:14px/1.5 system-ui,"Microsoft YaHei",sans-serif;color:var(--admin-text);background:var(--admin-page)}
|
|
6
|
-
*{box-sizing:border-box}body{margin:0}main{max-width:1080px;margin:auto;padding:28px 20px 60px}
|
|
7
|
-
header{display:flex;align-items:center;gap:20px;flex-wrap:wrap;margin-bottom:22px}
|
|
8
|
-
h1{font-size:27px;margin:0}h2{font-size:18px;margin:0 0 5px}
|
|
9
|
-
a,.row button{color:var(--admin-accent);text-decoration:none}a:hover{text-decoration:underline}
|
|
10
|
-
.card{background:var(--admin-surface);border:1px solid var(--admin-border);border-radius:var(--admin-card-radius);padding:var(--admin-card-padding);box-shadow:0 10px 24px #263a2d0a}
|
|
11
|
-
.subtle,.row small{color:var(--admin-muted)}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:16px}
|
|
12
|
-
.rows{display:grid;gap:8px}.row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,2fr) 160px;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--admin-row-border);border-radius:var(--admin-control-radius);background:var(--admin-row)}
|
|
13
|
-
.row button{border:0;background:none;text-align:left;padding:0;cursor:pointer;overflow-wrap:anywhere;font:inherit}.row strong{font-variant-numeric:tabular-nums}
|
|
14
|
-
button.pager{font:inherit;background:var(--admin-accent-surface);color:var(--admin-accent);border:1px solid var(--admin-border);border-radius:var(--admin-control-radius);padding:7px 12px;cursor:pointer}
|
|
15
|
-
button:disabled{opacity:.45;cursor:default}.pages{display:flex;align-items:center;gap:10px;margin-top:16px}.error{color:var(--admin-error)}.detail{margin-top:18px}.detail[hidden]{display:none}
|
|
16
|
-
@media(max-width:650px){main{padding:16px 12px}.card{padding:15px}.row{grid-template-columns:minmax(0,1fr)}.row small{grid-column:1/-1}}
|
|
17
|
-
</style></head><body><main><header><a href="/admin">← 中继管理</a><h1>使用统计</h1><span class="subtle">后台试运行统计,不限制使用</span></header><section class="card"><div class="toolbar"><div><h2>账号使用记录</h2><p id="count" class="subtle">正在读取…</p></div></div><div id="rows" class="rows"></div><div class="pages"><button id="prev" class="pager" type="button">上一页</button><span id="pageText"></span><button id="next" class="pager" type="button">下一页</button></div><p id="error" class="error" role="alert"></p></section><section id="detail" class="card detail" hidden><div class="toolbar"><div><h2 id="detailTitle"></h2><p class="subtle">按工作站查看此账号的统计</p></div><button id="closeDetail" class="pager" type="button">关闭</button></div><div id="detailRows" class="rows"></div></section></main><script>
|
|
18
|
-
const el=id=>document.getElementById(id);let page=1,total=0;
|
|
19
|
-
const duration=value=>{const seconds=Math.floor(Number(value)||0),h=Math.floor(seconds/3600),m=Math.floor(seconds%3600/60),s=seconds%60;return h?h+' 小时 '+m+' 分钟':m?m+' 分钟 '+s+' 秒':s+' 秒'};
|
|
20
|
-
const date=value=>value?new Date(value.replace(' ','T')+'Z').toLocaleString('zh-CN'):'—';
|
|
21
|
-
async function request(path){const token=sessionStorage.getItem('codeRelaxAdminToken');if(!token){location.href='/admin';return null}const response=await fetch(path,{headers:{Authorization:'Bearer '+token}});const body=await response.json();if(response.status===401||response.status===403){sessionStorage.removeItem('codeRelaxAdminToken');location.href='/admin';return null}if(!response.ok)throw new Error(body.error||'读取失败');return body}
|
|
22
|
-
const node=(tag,text,className='')=>{const item=document.createElement(tag);item.textContent=text;if(className)item.className=className;return item};
|
|
23
|
-
const metrics=row=>'消息 '+Number(row.message_count||0)+' · 轮次 '+Number(row.turn_count||0)+' · 任务 '+duration(row.task_seconds);
|
|
24
|
-
async function load(){el('error').textContent='';try{const result=await request('/admin/api/usage?page='+page);if(!result)return;total=result.total;el('count').textContent='共 '+total+' 个账号 · 每页 30 个';el('pageText').textContent='第 '+page+' / '+Math.max(1,Math.ceil(total/30))+' 页';el('prev').disabled=page<=1;el('next').disabled=page*30>=total;el('rows').replaceChildren();if(!result.rows.length){el('rows').append(node('p','暂无使用记录','subtle'));return}for(const row of result.rows){const item=node('div','','row'),button=node('button',row.email),time=node('strong',metrics(row)),last=node('small','最近使用 '+date(row.last_used_at));button.type='button';button.onclick=()=>detail(row.id);item.append(button,time,last);el('rows').append(item)}}catch(error){el('error').textContent=error.message}}
|
|
25
|
-
async function detail(id){try{const result=await request('/admin/api/usage?user='+encodeURIComponent(id));if(!result)return;el('detailTitle').textContent=result.user.email;el('detailRows').replaceChildren();if(!result.computers.length)el('detailRows').append(node('p','暂无使用记录','subtle'));for(const row of result.computers){const item=node('div','','row');item.append(node('span',row.computer_id),node('strong',metrics(row)),node('small','最近使用 '+date(row.last_used_at)));el('detailRows').append(item)}el('detail').hidden=false;el('detail').scrollIntoView({behavior:'smooth',block:'start'})}catch(error){el('error').textContent=error.message}}
|
|
26
|
-
el('prev').onclick=()=>{if(page>1){page--;void load()}};el('next').onclick=()=>{if(page*30<total){page++;void load()}};el('closeDetail').onclick=()=>{el('detail').hidden=true};void load();
|
|
27
|
-
</script></body></html>`;
|
|
28
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
const MAX_BATCH = 200;
|
|
2
|
-
|
|
3
|
-
export function usageDiagnostics(pool, onError = () => {}) {
|
|
4
|
-
let tail = Promise.resolve();
|
|
5
|
-
function serialize(action) {
|
|
6
|
-
const result = tail.then(action);
|
|
7
|
-
tail = result.catch(error => { onError(error); });
|
|
8
|
-
return result;
|
|
9
|
-
}
|
|
10
|
-
function record(event, detail) {
|
|
11
|
-
if (!/^[a-z-]{1,32}$/.test(event) || !/^[a-z-]{1,64}$/.test(detail)) throw new Error('Invalid usage diagnostic');
|
|
12
|
-
void serialize(async () => {
|
|
13
|
-
await pool.execute('INSERT INTO relay_usage_diagnostics (created_at,event,detail) VALUES (UTC_TIMESTAMP(3),?,?)', [event, detail]);
|
|
14
|
-
await pool.execute('DELETE FROM relay_usage_diagnostics WHERE created_at < UTC_TIMESTAMP(3) - INTERVAL 1 DAY');
|
|
15
|
-
}).catch(() => {});
|
|
16
|
-
}
|
|
17
|
-
async function read() {
|
|
18
|
-
return serialize(async () => {
|
|
19
|
-
const [rows] = await pool.query(`SELECT id,created_at,event,detail FROM relay_usage_diagnostics
|
|
20
|
-
ORDER BY id LIMIT ${MAX_BATCH + 1}`);
|
|
21
|
-
return { rows: rows.slice(0, MAX_BATCH), hasMore: rows.length > MAX_BATCH };
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
async function clear() {
|
|
25
|
-
return serialize(async () => {
|
|
26
|
-
const [result] = await pool.query('DELETE FROM relay_usage_diagnostics');
|
|
27
|
-
return { cleared: result.affectedRows };
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
async function drain() {
|
|
31
|
-
return serialize(async () => {
|
|
32
|
-
const db = await pool.getConnection();
|
|
33
|
-
try {
|
|
34
|
-
await db.beginTransaction();
|
|
35
|
-
const [rows] = await db.query(`SELECT id,created_at,event,detail FROM relay_usage_diagnostics
|
|
36
|
-
ORDER BY id LIMIT ${MAX_BATCH + 1} FOR UPDATE`);
|
|
37
|
-
const selected = rows.slice(0, MAX_BATCH);
|
|
38
|
-
if (selected.length) await db.execute('DELETE FROM relay_usage_diagnostics WHERE id <= ?', [selected.at(-1).id]);
|
|
39
|
-
await db.commit();
|
|
40
|
-
return { rows: selected, hasMore: rows.length > MAX_BATCH };
|
|
41
|
-
} catch (error) { await db.rollback(); throw error; }
|
|
42
|
-
finally { db.release(); }
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
return { record, read, clear, drain };
|
|
46
|
-
}
|