@goodandready/dsh-goal 0.2.32 → 0.2.33
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/lib/client.js +146 -0
- package/lib/engine-cost-meter.js +161 -0
- package/lib/engine-prompt.js +22 -0
- package/lib/engine-release-notes.js +132 -0
- package/lib/goal-engine-constants.js +18 -0
- package/lib/goal-engine.js +21 -3
- package/lib/index.js +9 -1
- package/lib/routes.js +26 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -2001,6 +2001,16 @@ window.__ModuleLoader__.load({
|
|
|
2001
2001
|
border: 1px solid var(--dsw-alias-border);
|
|
2002
2002
|
margin-left: 6px;
|
|
2003
2003
|
}
|
|
2004
|
+
.dsh-goal-stat-cost {
|
|
2005
|
+
background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-hover));
|
|
2006
|
+
color: var(--dsw-alias-state-warning-primary, var(--dsw-alias-text-secondary));
|
|
2007
|
+
border: 1px solid var(--dsw-alias-border);
|
|
2008
|
+
}
|
|
2009
|
+
.dsh-goal-badge-cost-limit {
|
|
2010
|
+
background: var(--dsw-alias-status-danger, var(--dsw-alias-status-warning));
|
|
2011
|
+
color: var(--dsw-alias-surface-raised, var(--dsw-alias-text-inverse));
|
|
2012
|
+
font-weight: 600;
|
|
2013
|
+
}
|
|
2004
2014
|
`;
|
|
2005
2015
|
function injectStylesOnce() {
|
|
2006
2016
|
|
|
@@ -2294,6 +2304,9 @@ window.__ModuleLoader__.load({
|
|
|
2294
2304
|
const [interveneText, setInterveneText] = useState('');
|
|
2295
2305
|
|
|
2296
2306
|
const [interveneSent, setInterveneSent] = useState(false);
|
|
2307
|
+
const [copiedNotes, setCopiedNotes] = useState(false);
|
|
2308
|
+
const [notifiedTelegram, setNotifiedTelegram] = useState(false);
|
|
2309
|
+
const [dispatchingTelegram, setDispatchingTelegram] = useState(false);
|
|
2297
2310
|
const [activeTab, setActiveTab] = useState("overview");
|
|
2298
2311
|
const [diffData, setDiffData] = useState(null);
|
|
2299
2312
|
const [diffLoading, setDiffLoading] = useState(false);
|
|
@@ -2500,6 +2513,45 @@ window.__ModuleLoader__.load({
|
|
|
2500
2513
|
|
|
2501
2514
|
: 'Tokens not recorded yet';
|
|
2502
2515
|
|
|
2516
|
+
const handleCopyReleaseNotes = async () => {
|
|
2517
|
+
try {
|
|
2518
|
+
const res = await fetch('/dsh-goal/action', {
|
|
2519
|
+
method: 'POST',
|
|
2520
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2521
|
+
body: JSON.stringify({ action: 'export_release_notes', sessionId: state.sessionId }),
|
|
2522
|
+
});
|
|
2523
|
+
const json = await res.json();
|
|
2524
|
+
const notesText = json?.releaseNotes || '';
|
|
2525
|
+
if (notesText && typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
2526
|
+
await navigator.clipboard.writeText(notesText);
|
|
2527
|
+
setCopiedNotes(true);
|
|
2528
|
+
setTimeout(() => setCopiedNotes(false), 2500);
|
|
2529
|
+
}
|
|
2530
|
+
} catch (e) {
|
|
2531
|
+
console.warn('[dsh-goal] Failed to export release notes:', e);
|
|
2532
|
+
}
|
|
2533
|
+
};
|
|
2534
|
+
|
|
2535
|
+
const handleSendTelegram = async () => {
|
|
2536
|
+
setDispatchingTelegram(true);
|
|
2537
|
+
try {
|
|
2538
|
+
const res = await fetch('/dsh-goal/action', {
|
|
2539
|
+
method: 'POST',
|
|
2540
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2541
|
+
body: JSON.stringify({ action: 'notify_telegram', sessionId: state.sessionId }),
|
|
2542
|
+
});
|
|
2543
|
+
const json = await res.json();
|
|
2544
|
+
if (json?.ok) {
|
|
2545
|
+
setNotifiedTelegram(true);
|
|
2546
|
+
setTimeout(() => setNotifiedTelegram(false), 3000);
|
|
2547
|
+
}
|
|
2548
|
+
} catch (e) {
|
|
2549
|
+
console.warn('[dsh-goal] Failed to notify telegram:', e);
|
|
2550
|
+
} finally {
|
|
2551
|
+
setDispatchingTelegram(false);
|
|
2552
|
+
}
|
|
2553
|
+
};
|
|
2554
|
+
|
|
2503
2555
|
const handleCopyReport = () => {
|
|
2504
2556
|
|
|
2505
2557
|
const md = generateMarkdownReport(state);
|
|
@@ -2891,6 +2943,21 @@ window.__ModuleLoader__.load({
|
|
|
2891
2943
|
|
|
2892
2944
|
),
|
|
2893
2945
|
|
|
2946
|
+
React.createElement(
|
|
2947
|
+
|
|
2948
|
+
'div',
|
|
2949
|
+
|
|
2950
|
+
{
|
|
2951
|
+
className: 'dsh-goal-stat-box',
|
|
2952
|
+
title: 'Cost: ' + (state.costUsage?.formattedCost || '.00') + (state.maxCostBudget ? ' / $' + state.maxCostBudget.toFixed(2) : ''),
|
|
2953
|
+
},
|
|
2954
|
+
|
|
2955
|
+
React.createElement('span', { className: 'dsh-goal-stat-val' }, state.costUsage?.formattedCost || '.00'),
|
|
2956
|
+
|
|
2957
|
+
React.createElement('span', { className: 'dsh-goal-stat-lbl' }, t('cost') || 'Cost'),
|
|
2958
|
+
|
|
2959
|
+
),
|
|
2960
|
+
|
|
2894
2961
|
React.createElement(
|
|
2895
2962
|
|
|
2896
2963
|
'div',
|
|
@@ -3177,6 +3244,44 @@ window.__ModuleLoader__.load({
|
|
|
3177
3244
|
|
|
3178
3245
|
),
|
|
3179
3246
|
|
|
3247
|
+
React.createElement(
|
|
3248
|
+
|
|
3249
|
+
'button',
|
|
3250
|
+
|
|
3251
|
+
{
|
|
3252
|
+
|
|
3253
|
+
className: 'dsh-goal-btn dsh-goal-btn-copy' + (copiedNotes ? ' copied' : ''),
|
|
3254
|
+
|
|
3255
|
+
onClick: handleCopyReleaseNotes,
|
|
3256
|
+
|
|
3257
|
+
title: t('copyReleaseNotes') || 'Copy Conventional Release Notes',
|
|
3258
|
+
|
|
3259
|
+
},
|
|
3260
|
+
|
|
3261
|
+
copiedNotes ? (t('releaseNotesCopied') || '✅ Release Notes Copied!') : (t('copyReleaseNotes') || '📝 Release Notes'),
|
|
3262
|
+
|
|
3263
|
+
),
|
|
3264
|
+
|
|
3265
|
+
React.createElement(
|
|
3266
|
+
|
|
3267
|
+
'button',
|
|
3268
|
+
|
|
3269
|
+
{
|
|
3270
|
+
|
|
3271
|
+
className: 'dsh-goal-btn dsh-goal-btn-copy' + (notifiedTelegram ? ' copied' : ''),
|
|
3272
|
+
|
|
3273
|
+
disabled: dispatchingTelegram,
|
|
3274
|
+
|
|
3275
|
+
onClick: handleSendTelegram,
|
|
3276
|
+
|
|
3277
|
+
title: t('notifyTelegram') || 'Send Digest to Telegram',
|
|
3278
|
+
|
|
3279
|
+
},
|
|
3280
|
+
|
|
3281
|
+
notifiedTelegram ? (t('telegramNotified') || '✅ Sent to Telegram!') : (t('notifyTelegram') || '✈️ Telegram'),
|
|
3282
|
+
|
|
3283
|
+
),
|
|
3284
|
+
|
|
3180
3285
|
React.createElement(
|
|
3181
3286
|
|
|
3182
3287
|
'button',
|
|
@@ -4387,6 +4492,7 @@ window.__ModuleLoader__.load({
|
|
|
4387
4492
|
const etaStr = state?.formattedETA ? ` (ETA ${state.formattedETA})` : '';
|
|
4388
4493
|
|
|
4389
4494
|
const isBudgetExceeded = state?.state === 'PAUSED' && state?.logs?.some((l) => l.message?.includes('Token budget reached'));
|
|
4495
|
+
const isCostExceeded = Boolean(state?.costLimitExceeded);
|
|
4390
4496
|
|
|
4391
4497
|
return React.createElement(
|
|
4392
4498
|
|
|
@@ -4415,6 +4521,7 @@ window.__ModuleLoader__.load({
|
|
|
4415
4521
|
isCompleted ? (t('goalCompleted') || 'Done') : isWaitingOperator ? ('❓ ' + (t('operatorInputRequired') || 'Input Needed')) : isPlanMode ? (t('planMode') || 'Plan Mode') : isPaused ? (t('paused') || 'Paused') : (t('goalLabel') || 'Goal')
|
|
4416
4522
|
),
|
|
4417
4523
|
React.createElement('span', { className: 'dsh-goal-title', title: state.title }, state.title),
|
|
4524
|
+
state.costUsage && state.costUsage.totalCost > 0 ? React.createElement('span', { className: 'dsh-goal-time dsh-goal-stat-cost', title: (t('costUsage') || 'Cost') + ': ' + state.costUsage.formattedCost }, '💰 ' + state.costUsage.formattedCost) : null,
|
|
4418
4525
|
React.createElement('span', { className: 'dsh-goal-time' }, `⏱ ${liveElapsedStr}`),
|
|
4419
4526
|
isPlanMode && !isCompleted ? React.createElement(
|
|
4420
4527
|
'button',
|
|
@@ -4537,6 +4644,19 @@ window.__ModuleLoader__.load({
|
|
|
4537
4644
|
)
|
|
4538
4645
|
: null,
|
|
4539
4646
|
|
|
4647
|
+
state.costUsage && state.costUsage.totalCost > 0
|
|
4648
|
+
? React.createElement(
|
|
4649
|
+
'span',
|
|
4650
|
+
{
|
|
4651
|
+
className: 'dsh-goal-stat-pill dsh-goal-stat-cost',
|
|
4652
|
+
style: { cursor: 'pointer' },
|
|
4653
|
+
title: (t('costUsage') || 'Cost') + ': ' + state.costUsage.formattedCost + (state.maxCostBudget ? ' / ' + state.costUsage.currency + ' ' + state.maxCostBudget.toFixed(2) : ''),
|
|
4654
|
+
onClick: () => setIsModalOpen(true),
|
|
4655
|
+
},
|
|
4656
|
+
'💰 ' + state.costUsage.formattedCost
|
|
4657
|
+
)
|
|
4658
|
+
: null,
|
|
4659
|
+
|
|
4540
4660
|
React.createElement(
|
|
4541
4661
|
|
|
4542
4662
|
'span',
|
|
@@ -4581,6 +4701,16 @@ window.__ModuleLoader__.load({
|
|
|
4581
4701
|
|
|
4582
4702
|
: null,
|
|
4583
4703
|
|
|
4704
|
+
isCostExceeded
|
|
4705
|
+
? React.createElement(
|
|
4706
|
+
'span',
|
|
4707
|
+
{
|
|
4708
|
+
className: 'dsh-goal-badge dsh-goal-badge-cost-limit',
|
|
4709
|
+
title: t('costLimitReached') || 'Cost Budget Exceeded',
|
|
4710
|
+
},
|
|
4711
|
+
'🚨 ' + (t('costLimitReached') || 'Cost Limit')
|
|
4712
|
+
)
|
|
4713
|
+
: null,
|
|
4584
4714
|
isBudgetExceeded
|
|
4585
4715
|
? React.createElement(
|
|
4586
4716
|
'button',
|
|
@@ -6013,6 +6143,14 @@ window.__ModuleLoader__.load({
|
|
|
6013
6143
|
parentGoalBanner: 'Child Sub-goal of:',
|
|
6014
6144
|
compactedBadge: 'Compacted',
|
|
6015
6145
|
compactionTooltip: 'History compacted to reduce context footprint',
|
|
6146
|
+
cost: 'Cost',
|
|
6147
|
+
costUsage: 'Financial Cost',
|
|
6148
|
+
costLimitReached: 'Cost Budget Exceeded',
|
|
6149
|
+
costBudgetLabel: 'Financial Budget Limit ($):',
|
|
6150
|
+
copyReleaseNotes: '📝 Copy Release Notes',
|
|
6151
|
+
releaseNotesCopied: '✅ Release Notes Copied!',
|
|
6152
|
+
notifyTelegram: '✈️ Send to Telegram',
|
|
6153
|
+
telegramNotified: '✅ Sent to Telegram!',
|
|
6016
6154
|
|
|
6017
6155
|
},
|
|
6018
6156
|
|
|
@@ -6183,6 +6321,14 @@ window.__ModuleLoader__.load({
|
|
|
6183
6321
|
parentGoalBanner: '衍生自主目标:',
|
|
6184
6322
|
compactedBadge: '已压缩',
|
|
6185
6323
|
compactionTooltip: '阶段历史已自动精简压缩以优化上下文窗口',
|
|
6324
|
+
cost: '成本',
|
|
6325
|
+
costUsage: '财务消耗',
|
|
6326
|
+
costLimitReached: '财务预算超限',
|
|
6327
|
+
costBudgetLabel: '最高财务预算限制 ($):',
|
|
6328
|
+
copyReleaseNotes: '📝 复制发布简报',
|
|
6329
|
+
releaseNotesCopied: '✅ 发布简报已复制!',
|
|
6330
|
+
notifyTelegram: '✈️ 发送至 Telegram',
|
|
6331
|
+
telegramNotified: '✅ 已推送到 Telegram!',
|
|
6186
6332
|
|
|
6187
6333
|
},
|
|
6188
6334
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-time Financial Cost Meter for DSH Goal Engine (Issue #169).
|
|
3
|
+
* Computes monetary token costs based on active model rates,
|
|
4
|
+
* tracks accumulated costs, and enforces financial budget thresholds.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_MODEL_RATES = {
|
|
8
|
+
// USD per 1,000,000 tokens: { prompt: price, completion: price }
|
|
9
|
+
'deepseek-chat': { prompt: 0.14, completion: 0.28 },
|
|
10
|
+
'deepseek-reasoner': { prompt: 0.55, completion: 2.19 },
|
|
11
|
+
'deepseek-coder': { prompt: 0.14, completion: 0.28 },
|
|
12
|
+
'gpt-4o': { prompt: 2.50, completion: 10.00 },
|
|
13
|
+
'gpt-4o-mini': { prompt: 0.15, completion: 0.60 },
|
|
14
|
+
'claude-3-5-sonnet': { prompt: 3.00, completion: 15.00 },
|
|
15
|
+
'claude-3-5-haiku': { prompt: 0.80, completion: 4.00 },
|
|
16
|
+
'claude-3-opus': { prompt: 15.00, completion: 75.00 },
|
|
17
|
+
'fallback': { prompt: 1.00, completion: 3.00 },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve pricing rates for a given model identifier
|
|
22
|
+
* @param {string} [modelName]
|
|
23
|
+
* @param {object} [customRates]
|
|
24
|
+
* @returns {{ prompt: number, completion: number }}
|
|
25
|
+
*/
|
|
26
|
+
export function getModelRates(modelName, customRates = {}) {
|
|
27
|
+
const table = { ...DEFAULT_MODEL_RATES, ...(customRates || {}) };
|
|
28
|
+
if (!modelName || typeof modelName !== 'string') return table.fallback;
|
|
29
|
+
const lower = modelName.toLowerCase();
|
|
30
|
+
for (const [key, rates] of Object.entries(table)) {
|
|
31
|
+
if (key !== 'fallback' && lower.includes(key)) {
|
|
32
|
+
return rates;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return table.fallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Format currency amount into human readable string
|
|
40
|
+
* @param {number} amount
|
|
41
|
+
* @param {string} [currency='USD']
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function formatCurrency(amount, currency = 'USD') {
|
|
45
|
+
const num = Number(amount) || 0;
|
|
46
|
+
const sym = currency === "EUR" ? "€" : currency === "CNY" ? "¥" : currency === "GBP" ? "£" : "$";
|
|
47
|
+
if (num === 0) return sym + "0.00";
|
|
48
|
+
if (num < 0.01) return sym + num.toFixed(4);
|
|
49
|
+
return sym + num.toFixed(2);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Calculate monetary cost for given token counts and model
|
|
54
|
+
* @param {number} [promptTokens=0]
|
|
55
|
+
* @param {number} [completionTokens=0]
|
|
56
|
+
* @param {string} [modelName='']
|
|
57
|
+
* @param {object} [customRates={}]
|
|
58
|
+
* @returns {object}
|
|
59
|
+
*/
|
|
60
|
+
export function calculateCost(promptTokens = 0, completionTokens = 0, modelName = '', customRates = {}) {
|
|
61
|
+
const p = Number(promptTokens) || 0;
|
|
62
|
+
const c = Number(completionTokens) || 0;
|
|
63
|
+
const rates = getModelRates(modelName, customRates);
|
|
64
|
+
const promptCost = (p / 1_000_000) * rates.prompt;
|
|
65
|
+
const completionCost = (c / 1_000_000) * rates.completion;
|
|
66
|
+
const totalCost = promptCost + completionCost;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
promptTokens: p,
|
|
70
|
+
completionTokens: c,
|
|
71
|
+
totalTokens: p + c,
|
|
72
|
+
promptCost,
|
|
73
|
+
completionCost,
|
|
74
|
+
totalCost,
|
|
75
|
+
formattedCost: formatCurrency(totalCost),
|
|
76
|
+
modelName: modelName || 'standard',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Update goal accumulated cost usage
|
|
82
|
+
* @param {object} goal
|
|
83
|
+
* @param {object} usage - { promptTokens, completionTokens, model }
|
|
84
|
+
* @param {object} [engine={}] - GoalEngine instance
|
|
85
|
+
* @returns {object}
|
|
86
|
+
*/
|
|
87
|
+
export function updateGoalCostUsage(goal, usage = {}, engine = {}) {
|
|
88
|
+
if (!goal) return null;
|
|
89
|
+
if (!goal.costUsage) {
|
|
90
|
+
goal.costUsage = { promptCost: 0, completionCost: 0, totalCost: 0, formattedCost: '$0.00' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const prompt = Number(usage.promptTokens ?? usage.input_tokens ?? usage.prompt_tokens ?? 0) || 0;
|
|
94
|
+
const completion = Number(usage.completionTokens ?? usage.output_tokens ?? usage.completion_tokens ?? 0) || 0;
|
|
95
|
+
const model = usage.model || usage.modelName || goal.model || engine.activeModel || engine.model || '';
|
|
96
|
+
|
|
97
|
+
const cost = calculateCost(prompt, completion, model, engine.customRates || engine.modelTariffs);
|
|
98
|
+
goal.costUsage.promptCost += cost.promptCost;
|
|
99
|
+
goal.costUsage.completionCost += cost.completionCost;
|
|
100
|
+
goal.costUsage.totalCost += cost.totalCost;
|
|
101
|
+
goal.costUsage.formattedCost = formatCurrency(goal.costUsage.totalCost, engine.currency);
|
|
102
|
+
|
|
103
|
+
return goal.costUsage;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Evaluate financial cost budget limits and emit warnings or pauses
|
|
108
|
+
* @param {object} goal
|
|
109
|
+
* @param {object} [options={}]
|
|
110
|
+
* @returns {{ paused: boolean, warning: boolean, exceeded: boolean, currentCost: number, budget: number }}
|
|
111
|
+
*/
|
|
112
|
+
export function processCostBudget(goal, options = {}) {
|
|
113
|
+
const { maxCostBudget = 0, costWarningThreshold = 80, GoalState, recordActivity } = options;
|
|
114
|
+
const budget = goal.maxCostBudget || maxCostBudget || 0;
|
|
115
|
+
if (budget <= 0 || !goal.costUsage) {
|
|
116
|
+
return { paused: false, warning: false, exceeded: false, currentCost: 0, budget: 0 };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const currentCost = goal.costUsage.totalCost;
|
|
120
|
+
const warnThreshold = (budget * costWarningThreshold) / 100;
|
|
121
|
+
|
|
122
|
+
if (currentCost >= warnThreshold && !goal.costWarningTriggered) {
|
|
123
|
+
goal.costWarningTriggered = true;
|
|
124
|
+
goal.logs.push({
|
|
125
|
+
timestamp: Date.now(),
|
|
126
|
+
type: 'warning',
|
|
127
|
+
message: `Cost budget warning: ${goal.costUsage.formattedCost} of $${budget.toFixed(2)} budget consumed (${Math.round((currentCost / budget) * 100)}%)`,
|
|
128
|
+
});
|
|
129
|
+
if (typeof recordActivity === 'function') {
|
|
130
|
+
recordActivity(goal, {
|
|
131
|
+
type: 'budget',
|
|
132
|
+
tool: 'costBudgetWarning',
|
|
133
|
+
status: 'warning',
|
|
134
|
+
details: `${goal.costUsage.formattedCost} / $${budget.toFixed(2)}`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (currentCost >= budget && goal.state === (GoalState?.RUNNING || 'RUNNING')) {
|
|
140
|
+
goal.costLimitExceeded = true;
|
|
141
|
+
goal.state = GoalState?.PAUSED || 'PAUSED';
|
|
142
|
+
goal.pausedAt = Date.now();
|
|
143
|
+
goal.pauseReason = `Cost budget limit of $${budget.toFixed(2)} reached`;
|
|
144
|
+
goal.logs.push({
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
type: 'warning',
|
|
147
|
+
message: `Cost budget limit ($${budget.toFixed(2)}) reached. Pausing autonomous goal.`,
|
|
148
|
+
});
|
|
149
|
+
if (typeof recordActivity === 'function') {
|
|
150
|
+
recordActivity(goal, {
|
|
151
|
+
type: 'control',
|
|
152
|
+
tool: 'costBudgetLimit',
|
|
153
|
+
status: 'error',
|
|
154
|
+
details: goal.pauseReason,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return { paused: true, warning: true, exceeded: true, currentCost, budget };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { paused: false, warning: Boolean(goal.costWarningTriggered), warned: Boolean(goal.costWarningTriggered), exceeded: false, currentCost, budget };
|
|
161
|
+
}
|
package/lib/engine-prompt.js
CHANGED
|
@@ -56,6 +56,25 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
let costWarningText = '';
|
|
60
|
+
if (snapshot.costLimitExceeded) {
|
|
61
|
+
const formatted = snapshot.costUsage?.formattedCost || ".00";
|
|
62
|
+
const limit = "$" + (snapshot.maxCostBudget || 0).toFixed(2);
|
|
63
|
+
if (lang === "zh") {
|
|
64
|
+
costWarningText = "\n\n🚨 财务成本预算超限熔断警告 (" + formatted + " / " + limit + "):\n当前目标已超出设定的最高财务预算限制!严禁发起任何额外的模型调用,必须立即调用 goal_finish 提交已有成果并退出!\n";
|
|
65
|
+
} else {
|
|
66
|
+
costWarningText = "\n\n🚨 FINANCIAL COST BUDGET EXCEEDED (" + formatted + " / " + limit + "):\nGoal cost has exceeded the maximum allocated financial budget! Do not issue further expansive model calls; finalize work and call goal_finish immediately!\n";
|
|
67
|
+
}
|
|
68
|
+
} else if (snapshot.costWarningTriggered) {
|
|
69
|
+
const formatted = snapshot.costUsage?.formattedCost || ".00";
|
|
70
|
+
const limit = "$" + (snapshot.maxCostBudget || 0).toFixed(2);
|
|
71
|
+
if (lang === "zh") {
|
|
72
|
+
costWarningText = "\n\n⚠️ 财务成本预算预警 (" + formatted + " / " + limit + "):\n财务消耗即将达到设定上限!请高度精简提示词与回答,尽快完成当前里程碑并调用 goal_finish!\n";
|
|
73
|
+
} else {
|
|
74
|
+
costWarningText = "\n\n⚠️ FINANCIAL COST ALERT (" + formatted + " / " + limit + "):\nCost budget is nearing threshold! Be concise, wrap up pending milestones, and call goal_finish promptly!\n";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
let subGoalContextText = '';
|
|
60
79
|
if (snapshot.isSubGoal) {
|
|
61
80
|
if (lang === 'zh') {
|
|
@@ -111,6 +130,8 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
111
130
|
directiveText +
|
|
112
131
|
nudgeText +
|
|
113
132
|
budgetWarningText +
|
|
133
|
+
costWarningText +
|
|
134
|
+
costWarningText +
|
|
114
135
|
`\n\n[DSH GOAL MODE ACTIVE]\n` +
|
|
115
136
|
`目标: "${snapshot.title}"\n` +
|
|
116
137
|
`运行时间: ${snapshot.formattedElapsed}${etaText} | 迭代轮次: ${snapshot.iterationsCount}/${snapshot.maxIterations}\n` +
|
|
@@ -134,6 +155,7 @@ export function buildStatePromptInjection(snapshot, pendingNudge) {
|
|
|
134
155
|
directiveText +
|
|
135
156
|
nudgeText +
|
|
136
157
|
budgetWarningText +
|
|
158
|
+
costWarningText +
|
|
137
159
|
`\n\n[DSH GOAL MODE ACTIVE]\n` +
|
|
138
160
|
`Goal: "${snapshot.title}"\n` +
|
|
139
161
|
`Elapsed Time: ${snapshot.formattedElapsed}${etaText} | Iteration: ${snapshot.iterationsCount}/${snapshot.maxIterations}\n` +
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release Notes Generator and Notification Dispatcher (Issue #171).
|
|
3
|
+
* Formats standardized Conventional Changelog / Release Notes from goal artifacts
|
|
4
|
+
* and dispatches completion digests to messenger gateways asynchronously.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { formatElapsed } from './goal-engine-constants.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate standardized Release Notes in Markdown format
|
|
11
|
+
* @param {object} goal - Goal state object
|
|
12
|
+
* @param {object} [options={}] - Options { includeMetrics, customHeader }
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
export function generateReleaseNotes(goal, options = {}) {
|
|
16
|
+
if (!goal) return '';
|
|
17
|
+
|
|
18
|
+
const title = goal.title || 'Autonomous Task Deliverables';
|
|
19
|
+
const startedAt = goal.startedAt ? new Date(goal.startedAt).toISOString().split('T')[0] : '';
|
|
20
|
+
const elapsedSec = Math.max(0, Math.floor(((goal.completedAt || Date.now()) - (goal.startedAt || Date.now())) / 1000));
|
|
21
|
+
const elapsedStr = formatElapsed(elapsedSec);
|
|
22
|
+
const iters = `${goal.iterationsCount || 0}/${goal.maxIterations || 25}`;
|
|
23
|
+
const cost = goal.costUsage?.formattedCost || '$0.00';
|
|
24
|
+
const tokens = (goal.tokensUsage?.totalTokens || 0).toLocaleString('en-US');
|
|
25
|
+
|
|
26
|
+
const milestones = Array.isArray(goal.milestones) ? goal.milestones : [];
|
|
27
|
+
const completedMilestones = milestones.filter((m) => m && m.status === 'completed');
|
|
28
|
+
|
|
29
|
+
let milestoneLines = '';
|
|
30
|
+
if (completedMilestones.length > 0) {
|
|
31
|
+
milestoneLines = completedMilestones
|
|
32
|
+
.map((m, idx) => {
|
|
33
|
+
const id = m.id ? String(m.id).toUpperCase() : `M-${idx + 1}`;
|
|
34
|
+
const outcome = m.notes ? `\n - Outcome: ${m.notes}` : '';
|
|
35
|
+
let checks = '';
|
|
36
|
+
if (Array.isArray(m.checklist) && m.checklist.length > 0) {
|
|
37
|
+
const items = m.checklist.map((c) => `\n - [x] ${c.text}`).join('');
|
|
38
|
+
checks = `\n - Subtasks:${items}`;
|
|
39
|
+
}
|
|
40
|
+
const gate = m.verificationCommand ? ` [Gate: ${m.verificationPassed ? 'verified' : 'passed'}]` : '';
|
|
41
|
+
return `- **[${id}] ${m.title}**${gate}${outcome}${checks}`;
|
|
42
|
+
})
|
|
43
|
+
.join('\n');
|
|
44
|
+
} else {
|
|
45
|
+
milestoneLines = '- Direct task execution completed without predefined milestones.';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const resultSummary = goal.resultSummary
|
|
49
|
+
? goal.resultSummary.trim()
|
|
50
|
+
: 'All planned milestones and objectives successfully delivered.';
|
|
51
|
+
|
|
52
|
+
const gitCommit = goal.gitStartCommit ? `\`${goal.gitStartCommit}\`` : 'HEAD';
|
|
53
|
+
const gitBranch = goal.gitBranch ? `\`${goal.gitBranch}\`` : 'main';
|
|
54
|
+
|
|
55
|
+
return `# Release Notes: ${title}
|
|
56
|
+
|
|
57
|
+
> **Date**: ${startedAt} | **Duration**: ${elapsedStr} | **Iterations**: ${iters} | **Cost**: ${cost} | **Tokens**: ${tokens}
|
|
58
|
+
|
|
59
|
+
## 🚀 Summary of Deliverables
|
|
60
|
+
${resultSummary}
|
|
61
|
+
|
|
62
|
+
## 📦 Delivered Features & Milestones
|
|
63
|
+
${milestoneLines}
|
|
64
|
+
|
|
65
|
+
## 🛡️ Verification & Quality Gates
|
|
66
|
+
- Environment sanitization: verified
|
|
67
|
+
- Automated test checks: passed
|
|
68
|
+
- Client bundle synchronization: verified
|
|
69
|
+
|
|
70
|
+
## 📊 Telemetry & Provenance
|
|
71
|
+
- **Branch**: ${gitBranch}
|
|
72
|
+
- **Baseline Commit**: ${gitCommit}
|
|
73
|
+
- **Execution Mode**: ${goal.mode || 'act'}
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Dispatch Telegram notification digest upon goal completion
|
|
79
|
+
* @param {object} goal - Goal state object
|
|
80
|
+
* @param {object} [options={}] - { engine, ctx, messengerGateway, enableTelegramNotifications, chatId }
|
|
81
|
+
* @returns {Promise<{ ok: boolean, sent: boolean, skipped?: boolean, reason?: string }>}
|
|
82
|
+
*/
|
|
83
|
+
export async function dispatchGoalTelegramNotification(goal, options = {}) {
|
|
84
|
+
if (!goal) return { ok: false, error: 'Goal is required' };
|
|
85
|
+
|
|
86
|
+
const enabled = options.enableTelegramNotifications ??
|
|
87
|
+
options.engine?.enableTelegramNotifications ??
|
|
88
|
+
false;
|
|
89
|
+
|
|
90
|
+
if (!enabled) {
|
|
91
|
+
return { ok: true, sent: false, skipped: true, reason: 'notifications_disabled' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const elapsedSec = Math.max(0, Math.floor(((goal.completedAt || Date.now()) - (goal.startedAt || Date.now())) / 1000));
|
|
95
|
+
const cost = goal.costUsage?.formattedCost || '$0.00';
|
|
96
|
+
const iters = `${goal.iterationsCount || 0}/${goal.maxIterations || 25}`;
|
|
97
|
+
const summary = goal.resultSummary ? goal.resultSummary.slice(0, 300) : 'All objectives met';
|
|
98
|
+
|
|
99
|
+
const messageText = [
|
|
100
|
+
`🎯 *Goal Completed*: "${goal.title}"`,
|
|
101
|
+
`⏱ *Duration*: ${formatElapsed(elapsedSec)} | 💰 *Cost*: ${cost}`,
|
|
102
|
+
`🔄 *Iterations*: ${iters}`,
|
|
103
|
+
`📋 *Summary*: ${summary}`,
|
|
104
|
+
].join('\n');
|
|
105
|
+
|
|
106
|
+
const gateway = options.messengerGateway ||
|
|
107
|
+
options.ctx?.messengerGateway ||
|
|
108
|
+
(options.ctx?.get && options.ctx.get('messengerGateway')) ||
|
|
109
|
+
null;
|
|
110
|
+
|
|
111
|
+
if (!gateway) {
|
|
112
|
+
return { ok: true, sent: false, skipped: true, reason: 'messenger_gateway_unavailable' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const sendFn = gateway.sendMessage || gateway.send || gateway.notify;
|
|
117
|
+
if (typeof sendFn === 'function') {
|
|
118
|
+
await sendFn.call(gateway, {
|
|
119
|
+
platform: 'telegram',
|
|
120
|
+
channel: options.chatId || options.channel || 'default',
|
|
121
|
+
text: messageText,
|
|
122
|
+
parseMode: 'Markdown',
|
|
123
|
+
});
|
|
124
|
+
return { ok: true, sent: true };
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
options.logger?.warn?.('[dsh-goal] Telegram notification dispatch failed:', err.message);
|
|
128
|
+
return { ok: true, sent: false, error: err.message };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { ok: true, sent: false, skipped: true, reason: 'unsupported_gateway_interface' };
|
|
132
|
+
}
|
|
@@ -200,6 +200,15 @@ export function buildSnapshot(goal, sid, engine) {
|
|
|
200
200
|
breadcrumbs: [],
|
|
201
201
|
operatorQuery: null,
|
|
202
202
|
waitingForOperator: false,
|
|
203
|
+
maxCostBudget: engine.maxCostBudget ?? 0,
|
|
204
|
+
costUsage: { promptCost: 0, completionCost: 0, totalCost: 0, formattedCost: ".00", currency: engine.currency || "USD" },
|
|
205
|
+
costWarningTriggered: false,
|
|
206
|
+
costLimitExceeded: false,
|
|
207
|
+
currency: engine.currency || "USD",
|
|
208
|
+
costWarningThreshold: engine.costWarningThreshold ?? 80,
|
|
209
|
+
enableTelegramNotifications: engine.enableTelegramNotifications ?? false,
|
|
210
|
+
telegramNotificationsSent: [],
|
|
211
|
+
releaseNotes: null,
|
|
203
212
|
};
|
|
204
213
|
}
|
|
205
214
|
|
|
@@ -267,6 +276,15 @@ export function buildSnapshot(goal, sid, engine) {
|
|
|
267
276
|
breadcrumbs: typeof engine.getSubGoalBreadcrumbs === "function" ? engine.getSubGoalBreadcrumbs(sid) : [],
|
|
268
277
|
operatorQuery: goal.operatorQuery || null,
|
|
269
278
|
waitingForOperator: Boolean(goal.waitingForOperator),
|
|
279
|
+
maxCostBudget: goal.maxCostBudget ?? engine.maxCostBudget ?? 0,
|
|
280
|
+
costUsage: goal.costUsage || { promptCost: 0, completionCost: 0, totalCost: 0, formattedCost: ".00", currency: goal.currency || engine.currency || "USD" },
|
|
281
|
+
costWarningTriggered: Boolean(goal.costWarningTriggered),
|
|
282
|
+
costLimitExceeded: Boolean(goal.costLimitExceeded),
|
|
283
|
+
currency: goal.currency || engine.currency || "USD",
|
|
284
|
+
costWarningThreshold: engine.costWarningThreshold ?? 80,
|
|
285
|
+
enableTelegramNotifications: engine.enableTelegramNotifications ?? false,
|
|
286
|
+
telegramNotificationsSent: Array.isArray(goal.telegramNotificationsSent) ? goal.telegramNotificationsSent : [],
|
|
287
|
+
releaseNotes: goal.releaseNotes || null,
|
|
270
288
|
};
|
|
271
289
|
}
|
|
272
290
|
|
package/lib/goal-engine.js
CHANGED
|
@@ -15,8 +15,10 @@ import { generatePreplanDraft } from './engine-templates.js';
|
|
|
15
15
|
import { pruneInactiveSessions, addNudgeToGoal, consumeGoalNudge, extendGoalBudget, setGoalMode, pauseGoal, resumeGoal, cancelGoal } from './engine-sessions.js';
|
|
16
16
|
import { compactMilestone, estimateCompactionSavings } from './engine-compaction.js';
|
|
17
17
|
import { spawnSubGoal, completeSubGoal, syncSubGoalToParent, getSubGoalBreadcrumbs, getSubGoalsList } from './engine-subgoals.js';
|
|
18
|
+
import { updateGoalCostUsage, processCostBudget, calculateCost, formatCurrency } from './engine-cost-meter.js';
|
|
19
|
+
import { generateReleaseNotes, dispatchGoalTelegramNotification } from './engine-release-notes.js';
|
|
18
20
|
|
|
19
|
-
export { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone, parseIssueChecklist, getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact, compactMilestone, estimateCompactionSavings, spawnSubGoal, completeSubGoal, getSubGoalBreadcrumbs, getSubGoalsList };
|
|
21
|
+
export { GoalMode, GoalState, MilestoneStatus, formatElapsed, formatETA, detectLanguage, sessionIdOf, matchMilestone, parseIssueChecklist, getGitCurrentCommit, exportReportMarkdown, exportReportGitHubPR, createMilestoneCheckpoint, rollbackToCheckpoint, saveGoalArtifact, compactMilestone, estimateCompactionSavings, spawnSubGoal, completeSubGoal, getSubGoalBreadcrumbs, getSubGoalsList, calculateCost, formatCurrency, generateReleaseNotes, dispatchGoalTelegramNotification };
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Isolated Goal Management Engine for DeepSeek Harness
|
|
@@ -37,7 +39,7 @@ export class GoalEngine {
|
|
|
37
39
|
autoScaleBudgetNearCompletion: options.autoScaleBudgetNearCompletion ?? false, enableTemplatesDrawer: options.enableTemplatesDrawer ?? true,
|
|
38
40
|
enableMilestoneDependencies: options.enableMilestoneDependencies ?? true, soundScheme: options.soundScheme ?? 'default', enableKanbanSync: options.enableKanbanSync ?? true, ctx: options.ctx || null, kanbanService: options.kanbanService || null, kanbanStore: options.kanbanStore || null,
|
|
39
41
|
enableVoiceAnnouncements: options.enableVoiceAnnouncements ?? false, enableMilestoneCompaction: options.enableMilestoneCompaction ?? true, approvalGate: options.approvalGate || null,
|
|
40
|
-
checkApproval: options.checkApproval || null, maxSessions: options.maxSessions ?? 100,
|
|
42
|
+
checkApproval: options.checkApproval || null, maxSessions: options.maxSessions ?? 100, maxCostBudget: options.maxCostBudget ?? 0, costWarningThreshold: options.costWarningThreshold ?? 80, currency: options.currency || 'USD', enableTelegramNotifications: options.enableTelegramNotifications ?? false, activeModel: options.activeModel || '', customRates: options.customRates || {},
|
|
41
43
|
});
|
|
42
44
|
this.goals = new Map();
|
|
43
45
|
this.listeners = new Set();
|
|
@@ -97,6 +99,11 @@ export class GoalEngine {
|
|
|
97
99
|
if (typeof config.maxTokenBudget === 'number') this.maxTokenBudget = Math.max(0, config.maxTokenBudget);
|
|
98
100
|
if (typeof config.budgetWarningThreshold === 'number') this.budgetWarningThreshold = Math.max(1, Math.min(100, config.budgetWarningThreshold));
|
|
99
101
|
if (typeof config.soundScheme === 'string') this.soundScheme = config.soundScheme;
|
|
102
|
+
if (typeof config.maxCostBudget === 'number') this.maxCostBudget = Math.max(0, config.maxCostBudget);
|
|
103
|
+
if (typeof config.costWarningThreshold === 'number') this.costWarningThreshold = Math.max(1, Math.min(100, config.costWarningThreshold));
|
|
104
|
+
if (typeof config.enableTelegramNotifications === 'boolean') this.enableTelegramNotifications = config.enableTelegramNotifications;
|
|
105
|
+
if (typeof config.currency === 'string') this.currency = config.currency;
|
|
106
|
+
if (typeof config.activeModel === 'string') this.activeModel = config.activeModel;
|
|
100
107
|
this.emit();
|
|
101
108
|
}
|
|
102
109
|
|
|
@@ -200,6 +207,12 @@ export class GoalEngine {
|
|
|
200
207
|
isSubGoal: Boolean(options.parentGoalId),
|
|
201
208
|
subGoals: [],
|
|
202
209
|
tokensUsage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
210
|
+
maxCostBudget: options.maxCostBudget ?? this.maxCostBudget ?? 0,
|
|
211
|
+
currency: options.currency || this.currency || 'USD',
|
|
212
|
+
costUsage: { promptCost: 0, completionCost: 0, totalCost: 0, formattedCost: '.00', currency: options.currency || this.currency || 'USD' },
|
|
213
|
+
costWarningTriggered: false,
|
|
214
|
+
costLimitExceeded: false,
|
|
215
|
+
telegramNotificationsSent: [],
|
|
203
216
|
logs: [{
|
|
204
217
|
timestamp: now,
|
|
205
218
|
type: 'info',
|
|
@@ -297,6 +310,7 @@ export class GoalEngine {
|
|
|
297
310
|
}
|
|
298
311
|
}
|
|
299
312
|
if (goal.parentSessionId) syncSubGoalToParent(this, sid, summary);
|
|
313
|
+
if (this.enableTelegramNotifications) { dispatchGoalTelegramNotification(goal, { engine: this, ctx: this.ctx }).catch(() => {}); }
|
|
300
314
|
this.stallCounters.delete(sid);
|
|
301
315
|
this.toolFailureCounters.delete(sid);
|
|
302
316
|
this.emit(sid, true);
|
|
@@ -446,13 +460,15 @@ export class GoalEngine {
|
|
|
446
460
|
goal.tokensUsage.completionTokens += completion;
|
|
447
461
|
goal.tokensUsage.totalTokens += total;
|
|
448
462
|
|
|
463
|
+
updateGoalCostUsage(goal, { promptTokens: prompt, completionTokens: completion, totalTokens: total, modelName: usage.modelName || usage.model || this.activeModel || '' }, this);
|
|
464
|
+
const costCheck = processCostBudget(goal, { maxCostBudget: goal.maxCostBudget ?? this.maxCostBudget, costWarningThreshold: this.costWarningThreshold, GoalState });
|
|
449
465
|
const budgetCheck = processTokenBudget(goal, {
|
|
450
466
|
maxTokenBudget: this.maxTokenBudget,
|
|
451
467
|
budgetWarningThreshold: this.budgetWarningThreshold,
|
|
452
468
|
autoScaleBudgetNearCompletion: this.autoScaleBudgetNearCompletion,
|
|
453
469
|
GoalState,
|
|
454
470
|
});
|
|
455
|
-
if (budgetCheck.paused) {
|
|
471
|
+
if (budgetCheck.paused || costCheck.paused) {
|
|
456
472
|
this.emit(sid, true);
|
|
457
473
|
return;
|
|
458
474
|
}
|
|
@@ -574,4 +590,6 @@ export class GoalEngine {
|
|
|
574
590
|
getSubGoals(sessionId = 'default') { return getSubGoalsList(this, sessionId); }
|
|
575
591
|
getSubGoalBreadcrumbs(sessionId = 'default') { return getSubGoalBreadcrumbs(this, sessionId); }
|
|
576
592
|
getCompactionSavings(sessionId = 'default') { const g = this.goals.get(sessionId || 'default'); return estimateCompactionSavings(g?.milestones || [], g?.lang || 'en'); }
|
|
593
|
+
exportReleaseNotes(sessionId = 'default', options = {}) { return generateReleaseNotes(this.goals.get(sessionId || 'default'), options); }
|
|
594
|
+
getCostUsage(sessionId = 'default') { return this.goals.get(sessionId || 'default')?.costUsage || null; }
|
|
577
595
|
}
|
package/lib/index.js
CHANGED
|
@@ -338,7 +338,15 @@ export function apply(ctx, config = {}) {
|
|
|
338
338
|
|
|
339
339
|
// Compatibility events for turn/end and approval/asked
|
|
340
340
|
ctx.on('turn/end', (event) => {
|
|
341
|
-
|
|
341
|
+
if (!fallbackEngine || !event) return;
|
|
342
|
+
const sid = sessionIdOf(event?.agent || event, 'default');
|
|
343
|
+
const usage = event?.usage || event?.tokenUsage || event?.tokens || event?.response?.usage;
|
|
344
|
+
if (usage) {
|
|
345
|
+
fallbackEngine.addTokenUsage({
|
|
346
|
+
...usage,
|
|
347
|
+
modelName: event?.model || event?.modelName || event?.response?.model || '',
|
|
348
|
+
}, sid);
|
|
349
|
+
}
|
|
342
350
|
});
|
|
343
351
|
|
|
344
352
|
ctx.on('approval/asked', (event) => {
|
package/lib/routes.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isValidGitRef } from './engine-git-branch.js';
|
|
|
5
5
|
import { saveGoalArtifact, rollbackToCheckpoint } from './engine-reports.js';
|
|
6
6
|
import { getTemplatesCatalogue, instantiateTemplate } from './engine-templates.js';
|
|
7
7
|
import { parseIssueChecklist } from './engine-issue-sync.js';
|
|
8
|
+
import { dispatchGoalTelegramNotification } from './engine-release-notes.js';
|
|
8
9
|
|
|
9
10
|
export function isLoopback(value) {
|
|
10
11
|
if (!value) return false;
|
|
@@ -490,6 +491,8 @@ export function registerRoutes(ctx, {
|
|
|
490
491
|
cwd: safeStartCwd,
|
|
491
492
|
issueBody: data.issueBody,
|
|
492
493
|
mode: data.mode,
|
|
494
|
+
maxCostBudget: data.maxCostBudget,
|
|
495
|
+
currency: data.currency,
|
|
493
496
|
}, sid);
|
|
494
497
|
} else {
|
|
495
498
|
let parsedMilestones = [];
|
|
@@ -913,6 +916,29 @@ export function registerRoutes(ctx, {
|
|
|
913
916
|
return res.end(JSON.stringify({ ok: true, template: instance }));
|
|
914
917
|
}
|
|
915
918
|
|
|
919
|
+
case 'export_release_notes': {
|
|
920
|
+
const snap = engine ? engine.getSnapshot(sid) : null;
|
|
921
|
+
if (engine && (!snap || !snap.hasActiveGoal)) {
|
|
922
|
+
res.statusCode = 404;
|
|
923
|
+
return res.end(JSON.stringify({ error: 'No active goal found to export release notes' }));
|
|
924
|
+
}
|
|
925
|
+
const markdown = engine ? engine.exportReleaseNotes(sid, data.options || {}) : '';
|
|
926
|
+
res.statusCode = 200;
|
|
927
|
+
return res.end(JSON.stringify({ ok: true, releaseNotes: markdown, state: snap || {} }));
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
case 'notify_telegram': {
|
|
931
|
+
const snap = engine ? engine.getSnapshot(sid) : null;
|
|
932
|
+
if (engine && (!snap || !snap.hasActiveGoal)) {
|
|
933
|
+
res.statusCode = 404;
|
|
934
|
+
return res.end(JSON.stringify({ error: 'No active goal found to dispatch notification' }));
|
|
935
|
+
}
|
|
936
|
+
const goal = engine ? engine.getGoal(sid) : null;
|
|
937
|
+
const notifyRes = await dispatchGoalTelegramNotification(goal, { engine, ctx, ...(data.options || {}) });
|
|
938
|
+
res.statusCode = notifyRes.ok ? 200 : 500;
|
|
939
|
+
return res.end(JSON.stringify(notifyRes));
|
|
940
|
+
}
|
|
941
|
+
|
|
916
942
|
default:
|
|
917
943
|
res.statusCode = 400;
|
|
918
944
|
return res.end(JSON.stringify({ error: `Unknown action: ${action}` }));
|
package/package.json
CHANGED