Show persistent operations queue

This commit is contained in:
wuyang
2026-11-04 10:11:04 +08:00
parent 830d5b722a
commit 713c3cd0a2
3 changed files with 33 additions and 1 deletions
+10
View File
@@ -118,6 +118,16 @@ async function openDaily() {
try { try {
const data = await api('/api/daily'); const data = await api('/api/daily');
host.replaceChildren(); host.replaceChildren();
try {
const queue = await api('/api/workqueue');
const work = el('section', undefined, 'report-section');
work.append(el('h2', '持续工作'));
const status = { pending: '待继续', running: '进行中', done: '已完成', blocked: '需要处理' };
const tasks = Array.isArray(queue.tasks) ? queue.tasks : [];
if (!tasks.length) work.append(el('p', '暂无任务。', 'muted'));
else { const list = el('ul'); for (const task of tasks) list.append(el('li', `${task.title} · ${status[task.status] || task.status} · ${task.updated_at ? dateTime(task.updated_at) : '时间待确认'}`)); work.append(list); }
host.append(work);
} catch { host.append(el('p', '暂时无法读取持续工作状态。', 'error')); }
if (data.invalid_count) host.append(el('p', `${data.invalid_count} 条记录无法读取,请检查数据文件。`, 'error')); if (data.invalid_count) host.append(el('p', `${data.invalid_count} 条记录无法读取,请检查数据文件。`, 'error'));
if (!data.records.length) host.append(el('p', '暂无每日记录。', 'empty')); if (!data.records.length) host.append(el('p', '暂无每日记录。', 'empty'));
for (const record of data.records) { for (const record of data.records) {
+9
View File
@@ -154,6 +154,15 @@ export function createApp(options = process.env) {
} }
if (route.startsWith('/api/')) { if (route.startsWith('/api/')) {
if (!session(req)) return json(res, 401, { error: '请先登录' }); if (!session(req)) return json(res, 401, { error: '请先登录' });
if (req.method === 'GET' && route === '/api/workqueue') {
const value = await jsonFile('workqueue.json', { schema: 1, tasks: [] });
if (value?.schema !== 1 || !Array.isArray(value.tasks)) return json(res, 500, { error: '工作队列格式无效' });
const valid = new Set(['pending', 'running', 'done', 'blocked']);
const tasks = value.tasks.filter(t => t && typeof t.id === 'string' && typeof t.title === 'string' && valid.has(t.status))
.map(t => ({ title: t.id.startsWith('interaction-') ? '核查新互动' : t.title.slice(0, 100), status: t.status,
attempts: Number.isInteger(t.attempts) ? t.attempts : 0, updated_at: typeof t.updated_at === 'string' ? t.updated_at : null }));
return json(res, 200, { tasks });
}
if (req.method === 'GET' && route === '/api/evolution') return json(res, 200, await jsonFile('evolution.json', { schema_version: 1, events: [] })); if (req.method === 'GET' && route === '/api/evolution') return json(res, 200, await jsonFile('evolution.json', { schema_version: 1, events: [] }));
if (req.method === 'GET' && route === '/api/daily') return json(res, 200, await dailyRecords()); if (req.method === 'GET' && route === '/api/daily') return json(res, 200, await dailyRecords());
if (req.method === 'GET' && route === '/api/dashboard') { if (req.method === 'GET' && route === '/api/dashboard') {
+14 -1
View File
@@ -43,7 +43,7 @@ test('password hashing uses random salts and checks a supplied password', () =>
test('anonymous users can see health/login shell but cannot read private reports or ledger', async t => { test('anonymous users can see health/login shell but cannot read private reports or ledger', async t => {
const { request } = await fixture(t); const { request } = await fixture(t);
for (const endpoint of ['/api/dashboard', '/api/evolution', '/api/reports/2026-W45', '/api/reports/2026-W45/download', '/api/ledger']) { for (const endpoint of ['/api/dashboard', '/api/evolution', '/api/workqueue', '/api/reports/2026-W45', '/api/reports/2026-W45/download', '/api/ledger']) {
const r = await request(endpoint); assert.equal(r.status, 401); assert.equal(r.headers.get('cache-control'), 'no-store'); const r = await request(endpoint); assert.equal(r.status, 401); assert.equal(r.headers.get('cache-control'), 'no-store');
assert.doesNotMatch(await r.text(), /PRIVATE|entries|cash_usd/); assert.doesNotMatch(await r.text(), /PRIVATE|entries|cash_usd/);
} }
@@ -52,6 +52,19 @@ test('anonymous users can see health/login shell but cannot read private reports
assert.equal((await request('/reports/2026-W45.json')).status, 404); assert.equal((await request('/reports/2026-W45.json')).status, 404);
}); });
test('work queue exposes only safe progress fields behind login', async t => {
const { directory, request, login } = await fixture(t);
await writeFile(path.join(directory, 'workqueue.json'), JSON.stringify({ schema: 1, tasks: [
{id:'interaction-private-a', title:'PRIVATE MESSAGE CONTENT', status:'pending', details:'PRIVATE MESSAGE', checkpoint:'PRIVATE CONTENT', attempts:1},
{id:'pilot',title:'动画试镜',status:'running',details:'SECRET',attempts:2,updated_at:'2026-11-04T01:00:00Z'}
] }));
assert.equal((await request('/api/workqueue')).status, 401);
const auth = await login(); const cookie = auth.headers.get('set-cookie').split(';')[0];
const response = await request('/api/workqueue', { headers: { cookie } });
const body = await response.text(); assert.doesNotMatch(body, /PRIVATE|SECRET/);
const data = JSON.parse(body); assert.equal(data.tasks[0].title, '核查新互动'); assert.equal(data.tasks[1].status, 'running');
});
test('login protects API and download, preserves unknown money, logout revokes copied cookie', async t => { test('login protects API and download, preserves unknown money, logout revokes copied cookie', async t => {
const { request, login } = await fixture(t); const { request, login } = await fixture(t);
const response = await login(); assert.equal(response.status, 200); const response = await login(); assert.equal(response.status, 200);