Show persistent operations queue
This commit is contained in:
@@ -118,6 +118,16 @@ async function openDaily() {
|
||||
try {
|
||||
const data = await api('/api/daily');
|
||||
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.records.length) host.append(el('p', '暂无每日记录。', 'empty'));
|
||||
for (const record of data.records) {
|
||||
|
||||
@@ -154,6 +154,15 @@ export function createApp(options = process.env) {
|
||||
}
|
||||
if (route.startsWith('/api/')) {
|
||||
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/daily') return json(res, 200, await dailyRecords());
|
||||
if (req.method === 'GET' && route === '/api/dashboard') {
|
||||
|
||||
@@ -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 => {
|
||||
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');
|
||||
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);
|
||||
});
|
||||
|
||||
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 => {
|
||||
const { request, login } = await fixture(t);
|
||||
const response = await login(); assert.equal(response.status, 200);
|
||||
|
||||
Reference in New Issue
Block a user