Include scheduled operations usage in weekly reports
This commit is contained in:
@@ -7,7 +7,7 @@ const el = (tag, text, className) => {
|
|||||||
};
|
};
|
||||||
let dashboard;
|
let dashboard;
|
||||||
let currentReportId;
|
let currentReportId;
|
||||||
const labels = { views: '视频播放', likes: '点赞', followers: '关注人数', downloads: 'APK 下载', visits: '网页访问', videos_published: '已发布视频', comments: '评论', favorites: '收藏', coins: '投币', watch_time: '观看时长', completion_rate: '完播率', revenue: '收益', value: '数值', unit: '单位', source: '来源', status: '状态', note: '备注', observed_at: '观测时间', project: '项目', account: '账号', account_followers: '账号关注人数' };
|
const labels = { views: '视频播放', likes: '点赞', followers: '关注人数', downloads: 'APK 下载', visits: '网页访问', videos_published: '已发布视频', comments: '评论', favorites: '收藏', coins: '投币', watch_time: '观看时长', completion_rate: '完播率', revenue: '收益', value: '数值', unit: '单位', source: '来源', status: '状态', note: '备注', observed_at: '观测时间', project: '项目', account: '账号', account_followers: '账号关注人数', scheduled_codex: '定时运营 Codex 用量', harness_new_runs: '后续 DeepSeek Harness 用量', codex: '项目会话 Codex 用量', deepseek: 'DeepSeek 历史用量快照' };
|
||||||
const basisLabels = { cash: '实际现金', allocated: '套餐分摊', estimate: '预估', settled: '已结算' };
|
const basisLabels = { cash: '实际现金', allocated: '套餐分摊', estimate: '预估', settled: '已结算' };
|
||||||
const categoryLabels = { subscription: '套餐', api: 'API', electricity: '电费', hosting: '托管', revenue: '收入', tokens: 'Token 用量' };
|
const categoryLabels = { subscription: '套餐', api: 'API', electricity: '电费', hosting: '托管', revenue: '收入', tokens: 'Token 用量' };
|
||||||
function valueText(value) {
|
function valueText(value) {
|
||||||
|
|||||||
+47
-1
@@ -117,6 +117,50 @@ def codex_usage(session_root, thread_id, start, end):
|
|||||||
'measurement_note': '会话日志计数含重复处理的上下文和缓存;不是唯一文字数、周额度百分比或API账单。推理token包含于输出计数,不重复相加。'}
|
'measurement_note': '会话日志计数含重复处理的上下文和缓存;不是唯一文字数、周额度百分比或API账单。推理token包含于输出计数,不重复相加。'}
|
||||||
|
|
||||||
|
|
||||||
|
def scheduled_codex_usage(directory, start, end):
|
||||||
|
"""Read bounded cycle summaries separately from the interactive thread."""
|
||||||
|
if not directory or not Path(directory).is_dir():
|
||||||
|
return {'available': False, 'reason': '定时运营汇总目录未配置或不可用'}
|
||||||
|
totals = {k: 0 for k in ('input_tokens', 'cached_input_tokens', 'output_tokens', 'reasoning_output_tokens')}
|
||||||
|
runs = failed = 0
|
||||||
|
for path in Path(directory).glob('*.summary.json'):
|
||||||
|
try:
|
||||||
|
record = json.loads(path.read_text())
|
||||||
|
stamp = record.get('started_at')
|
||||||
|
when = dt.datetime.fromisoformat(stamp.replace('Z', '+00:00')) if stamp else dt.datetime.strptime(record['id'][:16], '%Y%m%dT%H%M%SZ').replace(tzinfo=dt.timezone.utc)
|
||||||
|
if when.tzinfo is None or not start <= when < end:
|
||||||
|
continue
|
||||||
|
runs += 1
|
||||||
|
failed += record.get('status') != 'complete'
|
||||||
|
for usage in record.get('usage', []):
|
||||||
|
for k in totals:
|
||||||
|
totals[k] += max(0, int(usage.get(k, 0)))
|
||||||
|
except (ValueError, OSError, KeyError, TypeError, AttributeError):
|
||||||
|
continue
|
||||||
|
return {'available': True, 'runs': runs, 'failed_runs': failed, **totals,
|
||||||
|
'measurement_note': '来自独立定时会话的运行汇总,与交互会话分开统计;失败或中断可能没有完整用量。'}
|
||||||
|
|
||||||
|
|
||||||
|
def harness_run_usage(path, start, end):
|
||||||
|
if not Path(path).exists():
|
||||||
|
return {'available': False, 'reason': '后续 Harness 调用账本尚未创建'}
|
||||||
|
result = {'available': True, 'runs': 0, 'input_tokens': 0, 'output_tokens': 0, 'cash_settlement_usd': None}
|
||||||
|
for line in Path(path).read_text().splitlines():
|
||||||
|
try:
|
||||||
|
row = json.loads(line)
|
||||||
|
when = dt.datetime.fromisoformat(row['at_utc'].replace('Z', '+00:00'))
|
||||||
|
if when.tzinfo is None or not start <= when < end:
|
||||||
|
continue
|
||||||
|
result['runs'] += 1
|
||||||
|
for usage in row.get('usage') or []:
|
||||||
|
result['input_tokens'] += max(0, int(usage.get('inputTokens', 0)))
|
||||||
|
result['output_tokens'] += max(0, int(usage.get('outputTokens', 0)))
|
||||||
|
except (ValueError, KeyError, TypeError, AttributeError):
|
||||||
|
continue
|
||||||
|
result['measurement_note'] = '只统计自动账本创建后的调用;实际结算未知,不与历史快照重复计费。'
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def build_report(data_dir, snapshot=False, now=None):
|
def build_report(data_dir, snapshot=False, now=None):
|
||||||
now = now or dt.datetime.now(TZ)
|
now = now or dt.datetime.now(TZ)
|
||||||
now = now.astimezone(TZ)
|
now = now.astimezone(TZ)
|
||||||
@@ -184,8 +228,10 @@ def build_report(data_dir, snapshot=False, now=None):
|
|||||||
'project_video_views': 0 if not published else None,
|
'project_video_views': 0 if not published else None,
|
||||||
'project_game_sessions': None,
|
'project_game_sessions': None,
|
||||||
'codex': usage,
|
'codex': usage,
|
||||||
|
'scheduled_codex': scheduled_codex_usage(collector.get('ops_cycle_summaries_dir'), begin_stamp, end_stamp),
|
||||||
'deepseek': api or {'status': '未采集'},
|
'deepseek': api or {'status': '未采集'},
|
||||||
'measurement_notes': ['只统计运营账本登记稿件,不计入账户其他历史内容。', '当前会话包含PawPlay、运营基础设施和虚拟人设研究,token仅能按该共享会话汇总,未精确归因到单个方向。', '游戏离线运行且未接入统计,不能推导活跃、留存或转化率。', '已发布稿件存在时,播放数据需接入稿件快照采集,当前不填造数值。']
|
'harness_new_runs': harness_run_usage(data_dir / 'harness-runs.jsonl', begin_stamp, end_stamp),
|
||||||
|
'measurement_notes': ['只统计运营账本登记稿件,不计入账户其他历史内容。', '当前会话包含PawPlay、运营基础设施和虚拟人设研究,token仅能按该共享会话汇总,未精确归因到单个方向。定时任务作为独立会话分别统计。', '游戏离线运行且未接入统计,不能推导活跃、留存或转化率。', '已发布稿件存在时,播放数据需接入稿件快照采集,当前不填造数值。']
|
||||||
},
|
},
|
||||||
'costs': {
|
'costs': {
|
||||||
'cash_usd': round(sum(e['amount'] for e in costs), 2) if config.get('cash_tracking_complete') else None,
|
'cash_usd': round(sum(e['amount'] for e in costs), 2) if config.get('cash_tracking_complete') else None,
|
||||||
|
|||||||
@@ -53,6 +53,26 @@ class AccountingTests(unittest.TestCase):
|
|||||||
self.assertFalse(created)
|
self.assertFalse(created)
|
||||||
self.assertEqual(original, second.read_bytes())
|
self.assertEqual(original, second.read_bytes())
|
||||||
|
|
||||||
|
def test_scheduled_usage_is_period_scoped_and_preserves_failed_runs(self):
|
||||||
|
import json
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
(root / 'one.summary.json').write_text(json.dumps({'id':'20261103T141341Z-a','status':'complete','usage':[{'input_tokens':100,'cached_input_tokens':80,'output_tokens':10}]}))
|
||||||
|
(root / 'two.summary.json').write_text(json.dumps({'id':'20261104T141341Z-b','status':'failed','usage':[]}))
|
||||||
|
(root / 'old.summary.json').write_text(json.dumps({'id':'20261003T141341Z-c','status':'complete','usage':[{'input_tokens':900}]}))
|
||||||
|
value=report.scheduled_codex_usage(root,dt.datetime(2026,11,3,tzinfo=report.TZ),dt.datetime(2026,11,5,tzinfo=report.TZ))
|
||||||
|
self.assertEqual((value['runs'],value['failed_runs'],value['input_tokens'],value['cached_input_tokens']), (2,1,100,80))
|
||||||
|
|
||||||
|
def test_harness_ledger_counts_only_selected_period(self):
|
||||||
|
import json
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
path = Path(directory) / 'harness-runs.jsonl'
|
||||||
|
entries = [{'at_utc':'2026-11-03T14:00:00Z','usage':[{'inputTokens':10,'outputTokens':4}]}, {'at_utc':'2026-10-03T14:00:00Z','usage':[{'inputTokens':100}]}]
|
||||||
|
path.write_text('\n'.join(json.dumps(e) for e in entries))
|
||||||
|
value=report.harness_run_usage(path,dt.datetime(2026,11,3,tzinfo=report.TZ),dt.datetime(2026,11,5,tzinfo=report.TZ))
|
||||||
|
self.assertEqual((value['runs'],value['input_tokens'],value['output_tokens']),(1,10,4))
|
||||||
|
self.assertIsNone(value['cash_settlement_usd'])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user