@qnote/q-ai-note 1.0.0
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/README.md +50 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +55 -0
- package/dist/cli.js.map +1 -0
- package/dist/server/aiClient.d.ts +11 -0
- package/dist/server/aiClient.d.ts.map +1 -0
- package/dist/server/aiClient.js +83 -0
- package/dist/server/aiClient.js.map +1 -0
- package/dist/server/api/batchRecovery.d.ts +11 -0
- package/dist/server/api/batchRecovery.d.ts.map +1 -0
- package/dist/server/api/batchRecovery.js +68 -0
- package/dist/server/api/batchRecovery.js.map +1 -0
- package/dist/server/api/chat.d.ts +3 -0
- package/dist/server/api/chat.d.ts.map +1 -0
- package/dist/server/api/chat.js +485 -0
- package/dist/server/api/chat.js.map +1 -0
- package/dist/server/api/diary.d.ts +3 -0
- package/dist/server/api/diary.d.ts.map +1 -0
- package/dist/server/api/diary.js +102 -0
- package/dist/server/api/diary.js.map +1 -0
- package/dist/server/api/sandbox.d.ts +3 -0
- package/dist/server/api/sandbox.d.ts.map +1 -0
- package/dist/server/api/sandbox.js +87 -0
- package/dist/server/api/sandbox.js.map +1 -0
- package/dist/server/api/settings.d.ts +3 -0
- package/dist/server/api/settings.d.ts.map +1 -0
- package/dist/server/api/settings.js +45 -0
- package/dist/server/api/settings.js.map +1 -0
- package/dist/server/api/workItem.d.ts +3 -0
- package/dist/server/api/workItem.d.ts.map +1 -0
- package/dist/server/api/workItem.js +290 -0
- package/dist/server/api/workItem.js.map +1 -0
- package/dist/server/chatUtils.d.ts +15 -0
- package/dist/server/chatUtils.d.ts.map +1 -0
- package/dist/server/chatUtils.js +52 -0
- package/dist/server/chatUtils.js.map +1 -0
- package/dist/server/config.d.ts +14 -0
- package/dist/server/config.d.ts.map +1 -0
- package/dist/server/config.js +56 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/db.d.ts +6 -0
- package/dist/server/db.d.ts.map +1 -0
- package/dist/server/db.js +106 -0
- package/dist/server/db.js.map +1 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +72 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/react/agent.d.ts +56 -0
- package/dist/server/react/agent.d.ts.map +1 -0
- package/dist/server/react/agent.js +219 -0
- package/dist/server/react/agent.js.map +1 -0
- package/dist/server/react/prompts.d.ts +13 -0
- package/dist/server/react/prompts.d.ts.map +1 -0
- package/dist/server/react/prompts.js +84 -0
- package/dist/server/react/prompts.js.map +1 -0
- package/dist/server/react/tools.d.ts +67 -0
- package/dist/server/react/tools.d.ts.map +1 -0
- package/dist/server/react/tools.js +208 -0
- package/dist/server/react/tools.js.map +1 -0
- package/dist/server/types.d.ts +59 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/dist/web/app.js +1081 -0
- package/dist/web/chatView.js +31 -0
- package/dist/web/index.html +218 -0
- package/dist/web/shared.js +49 -0
- package/dist/web/styles.css +1320 -0
- package/dist/web/vueRenderers.js +191 -0
- package/package.json +46 -0
package/dist/web/app.js
ADDED
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
import { API_BASE, apiRequest, escapeHtml, safeText, appendLoadingMessage, setButtonState } from './shared.js';
|
|
2
|
+
import { renderChatEntry } from './chatView.js';
|
|
3
|
+
import { mountSandboxGrid, mountHtmlList, mountDiaryTimeline, mountWorkTree } from './vueRenderers.js';
|
|
4
|
+
|
|
5
|
+
const state = {
|
|
6
|
+
sandboxes: [],
|
|
7
|
+
currentSandbox: null,
|
|
8
|
+
diaries: [],
|
|
9
|
+
operations: [],
|
|
10
|
+
chats: [],
|
|
11
|
+
settings: {},
|
|
12
|
+
pendingAction: null,
|
|
13
|
+
workItemSearch: '',
|
|
14
|
+
workItemStatusFilter: 'all',
|
|
15
|
+
diarySearch: '',
|
|
16
|
+
diaryProcessedFilter: 'all',
|
|
17
|
+
changesSandboxFilter: '',
|
|
18
|
+
changesTypeFilter: 'all',
|
|
19
|
+
changesQuickFilter: 'all',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const expandedNodes = new Set();
|
|
23
|
+
let selectedNodeId = null;
|
|
24
|
+
|
|
25
|
+
function applyWorkItemFilters(items) {
|
|
26
|
+
const query = state.workItemSearch.trim().toLowerCase();
|
|
27
|
+
const statusFilter = state.workItemStatusFilter;
|
|
28
|
+
if (!query && statusFilter === 'all') {
|
|
29
|
+
return items;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const byId = new Map(items.map((item) => [item.id, item]));
|
|
33
|
+
const included = new Set();
|
|
34
|
+
|
|
35
|
+
function includeAncestors(item) {
|
|
36
|
+
let current = item;
|
|
37
|
+
while (current) {
|
|
38
|
+
included.add(current.id);
|
|
39
|
+
current = current.parent_id ? byId.get(current.parent_id) : null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
const statusMatched = statusFilter === 'all' || item.status === statusFilter;
|
|
45
|
+
const text = `${item.name || ''} ${item.description || ''} ${item.assignee || ''}`.toLowerCase();
|
|
46
|
+
const queryMatched = !query || text.includes(query);
|
|
47
|
+
if (statusMatched && queryMatched) {
|
|
48
|
+
includeAncestors(item);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return items.filter((item) => included.has(item.id));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function applyDiaryFilters(diaries) {
|
|
56
|
+
const query = state.diarySearch.trim().toLowerCase();
|
|
57
|
+
const processed = state.diaryProcessedFilter;
|
|
58
|
+
return diaries.filter((diary) => {
|
|
59
|
+
const queryMatched = !query || `${diary.content || ''}`.toLowerCase().includes(query);
|
|
60
|
+
const statusMatched = processed === 'all'
|
|
61
|
+
|| (processed === 'processed' && diary.processed)
|
|
62
|
+
|| (processed === 'unprocessed' && !diary.processed);
|
|
63
|
+
return queryMatched && statusMatched;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function expandAllNodes() {
|
|
68
|
+
const items = state.currentSandbox?.items || [];
|
|
69
|
+
items.forEach(item => expandedNodes.add(item.id));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderWorkTree() {
|
|
73
|
+
const tree = document.getElementById('work-tree');
|
|
74
|
+
if (!tree || !state.currentSandbox) return;
|
|
75
|
+
|
|
76
|
+
const allItems = state.currentSandbox.items || [];
|
|
77
|
+
const items = applyWorkItemFilters(allItems);
|
|
78
|
+
|
|
79
|
+
if (expandedNodes.size === 0 && allItems.length > 0) {
|
|
80
|
+
expandAllNodes();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (allItems.length === 0) {
|
|
84
|
+
tree.innerHTML = '<div class="empty-state"><p>点击上方"添加"按钮创建第一个任务</p></div>';
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
mountWorkTree('work-tree', {
|
|
89
|
+
items,
|
|
90
|
+
expandedIds: Array.from(expandedNodes),
|
|
91
|
+
onToggleExpand: (id) => {
|
|
92
|
+
if (expandedNodes.has(id)) {
|
|
93
|
+
expandedNodes.delete(id);
|
|
94
|
+
} else {
|
|
95
|
+
expandedNodes.add(id);
|
|
96
|
+
}
|
|
97
|
+
renderWorkTree();
|
|
98
|
+
},
|
|
99
|
+
onAddChild: (parentId) => {
|
|
100
|
+
document.getElementById('item-dialog-title').textContent = '添加子任务';
|
|
101
|
+
document.getElementById('item-dialog').dataset.editId = '';
|
|
102
|
+
document.getElementById('new-item-name').value = '';
|
|
103
|
+
document.getElementById('new-item-desc').value = '';
|
|
104
|
+
document.getElementById('new-item-assignee').value = '';
|
|
105
|
+
document.getElementById('new-item-status').value = 'pending';
|
|
106
|
+
document.getElementById('new-item-priority').value = 'medium';
|
|
107
|
+
document.getElementById('new-item-parent').value = parentId;
|
|
108
|
+
document.getElementById('item-dialog').showModal();
|
|
109
|
+
},
|
|
110
|
+
onEdit: (id) => {
|
|
111
|
+
editWorkItem(id);
|
|
112
|
+
},
|
|
113
|
+
onDelete: async (id) => {
|
|
114
|
+
if (confirm('确定删除此任务?')) {
|
|
115
|
+
await apiRequest(`${API_BASE}/items/${id}`, { method: 'DELETE' });
|
|
116
|
+
await loadSandbox(state.currentSandbox.id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
populateParentSelect(allItems);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function populateParentSelect(items) {
|
|
125
|
+
const select = document.getElementById('new-item-parent');
|
|
126
|
+
if (!select) return;
|
|
127
|
+
|
|
128
|
+
select.innerHTML = '<option value="">无(顶级)</option>' +
|
|
129
|
+
items.map(i => `<option value="${i.id}">${safeText(i.name)}</option>`).join('');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function loadSandboxes() {
|
|
133
|
+
state.sandboxes = await apiRequest(`${API_BASE}/sandboxes`);
|
|
134
|
+
renderSandboxes();
|
|
135
|
+
renderSandboxesSummary();
|
|
136
|
+
updateSandboxSelect();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function renderSandboxesSummary() {
|
|
140
|
+
const container = document.getElementById('sandboxes-summary');
|
|
141
|
+
if (!container) return;
|
|
142
|
+
const total = state.sandboxes.length;
|
|
143
|
+
const lastUpdated = total
|
|
144
|
+
? new Date(Math.max(...state.sandboxes.map((s) => new Date(s.updated_at).getTime()))).toLocaleString()
|
|
145
|
+
: '-';
|
|
146
|
+
const recent = state.sandboxes.filter((s) => Date.now() - new Date(s.updated_at).getTime() < 7 * 24 * 3600 * 1000).length;
|
|
147
|
+
|
|
148
|
+
container.innerHTML = `
|
|
149
|
+
<div class="summary-card"><div class="label">总沙盘数</div><div class="value">${total}</div></div>
|
|
150
|
+
<div class="summary-card"><div class="label">近7天活跃</div><div class="value">${recent}</div></div>
|
|
151
|
+
<div class="summary-card"><div class="label">最近更新</div><div class="value" style="font-size:13px;font-weight:500">${safeText(lastUpdated)}</div></div>
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderSandboxes() {
|
|
156
|
+
const grid = document.getElementById('sandbox-grid');
|
|
157
|
+
if (!grid) return;
|
|
158
|
+
|
|
159
|
+
mountSandboxGrid('sandbox-grid', {
|
|
160
|
+
sandboxes: state.sandboxes,
|
|
161
|
+
emptyText: '暂无沙盘',
|
|
162
|
+
onOpen: (id) => {
|
|
163
|
+
window.location.hash = `/sandbox/${id}`;
|
|
164
|
+
},
|
|
165
|
+
onDelete: async (id) => {
|
|
166
|
+
if (confirm('确定删除此沙盘?')) {
|
|
167
|
+
await apiRequest(`${API_BASE}/sandboxes/${id}`, { method: 'DELETE' });
|
|
168
|
+
await loadSandboxes();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function loadSandbox(id) {
|
|
175
|
+
const [sandbox, items, diaries] = await Promise.all([
|
|
176
|
+
apiRequest(`${API_BASE}/sandboxes/${id}`),
|
|
177
|
+
apiRequest(`${API_BASE}/sandboxes/${id}/items`),
|
|
178
|
+
apiRequest(`${API_BASE}/diaries?sandbox_id=${id}`)
|
|
179
|
+
]);
|
|
180
|
+
state.currentSandbox = { ...sandbox, items, diaries };
|
|
181
|
+
|
|
182
|
+
document.getElementById('sandbox-title').textContent = sandbox.name;
|
|
183
|
+
renderSandboxOverview();
|
|
184
|
+
renderWorkTree();
|
|
185
|
+
loadSandboxChats(id);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function renderSandboxOverview() {
|
|
189
|
+
const container = document.getElementById('sandbox-overview');
|
|
190
|
+
if (!container || !state.currentSandbox) return;
|
|
191
|
+
const items = state.currentSandbox.items || [];
|
|
192
|
+
const diaries = state.currentSandbox.diaries || [];
|
|
193
|
+
const byStatus = {
|
|
194
|
+
pending: items.filter((i) => i.status === 'pending').length,
|
|
195
|
+
inProgress: items.filter((i) => i.status === 'in_progress').length,
|
|
196
|
+
done: items.filter((i) => i.status === 'done').length,
|
|
197
|
+
};
|
|
198
|
+
container.innerHTML = `
|
|
199
|
+
<div class="summary-card"><div class="label">任务总数</div><div class="value">${items.length}</div></div>
|
|
200
|
+
<div class="summary-card"><div class="label">待处理/进行中</div><div class="value">${byStatus.pending}/${byStatus.inProgress}</div></div>
|
|
201
|
+
<div class="summary-card"><div class="label">已完成</div><div class="value">${byStatus.done}</div></div>
|
|
202
|
+
<div class="summary-card"><div class="label">关联日记</div><div class="value">${diaries.length}</div></div>
|
|
203
|
+
`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function loadSandboxChats(sandboxId) {
|
|
207
|
+
const messages = document.getElementById('sandbox-chat-messages');
|
|
208
|
+
if (!messages) return;
|
|
209
|
+
|
|
210
|
+
const chats = await apiRequest(`${API_BASE}/chats/sandbox/${sandboxId}`);
|
|
211
|
+
state.chats = chats;
|
|
212
|
+
|
|
213
|
+
mountHtmlList('sandbox-chat-messages', chats.map((chat) => renderChatEntry(chat, { safeText, renderAIActionMessage })));
|
|
214
|
+
|
|
215
|
+
messages.scrollTop = messages.scrollHeight;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function renderAIActionMessage(action) {
|
|
219
|
+
const actionType = action.action;
|
|
220
|
+
const getExecutionStatus = (targetAction) => {
|
|
221
|
+
const status = targetAction?.execution_result?.status;
|
|
222
|
+
if (status === 'success' || status === 'partial' || status === 'failed') {
|
|
223
|
+
return status;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const observation = String(targetAction?.observation || '');
|
|
227
|
+
const match = observation.match(/成功\s*(\d+)\s*\/\s*(\d+)/);
|
|
228
|
+
if (match) {
|
|
229
|
+
const successCount = Number(match[1]);
|
|
230
|
+
const totalCount = Number(match[2]);
|
|
231
|
+
if (Number.isFinite(successCount) && Number.isFinite(totalCount) && totalCount > 0) {
|
|
232
|
+
if (successCount >= totalCount) return 'success';
|
|
233
|
+
if (successCount <= 0) return 'failed';
|
|
234
|
+
return 'partial';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (/失败[::]/.test(observation)) {
|
|
239
|
+
return 'failed';
|
|
240
|
+
}
|
|
241
|
+
return 'success';
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (actionType === 'response' || actionType === 'clarify') {
|
|
245
|
+
return `<div class="chat-message assistant">${escapeHtml(action.response || action.observation || '')}</div>`;
|
|
246
|
+
}
|
|
247
|
+
else if (actionType === 'confirm' && action.confirm_items) {
|
|
248
|
+
// Skip confirm, go directly to done
|
|
249
|
+
return '';
|
|
250
|
+
}
|
|
251
|
+
else if (actionType === 'done') {
|
|
252
|
+
const status = getExecutionStatus(action);
|
|
253
|
+
const statusIcon = status === 'success' ? '✅' : status === 'partial' ? '⚠️' : '❌';
|
|
254
|
+
const statusClass = status === 'success' ? 'is-success' : status === 'partial' ? 'is-partial' : 'is-failed';
|
|
255
|
+
const undoBtn = action.operationId
|
|
256
|
+
? `<button class="btn btn-sm btn-undo" data-operation-id="${action.operationId}" onclick="undoOperation('${action.operationId}', this)">撤销</button>`
|
|
257
|
+
: '';
|
|
258
|
+
return `<div class="chat-message assistant ${status === 'failed' ? 'error' : ''}">
|
|
259
|
+
<div class="operation-result ${statusClass}">
|
|
260
|
+
<span class="status-icon">${statusIcon}</span>
|
|
261
|
+
<span class="result-text">${escapeHtml(action.observation || '操作完成')}</span>
|
|
262
|
+
${undoBtn}
|
|
263
|
+
</div>
|
|
264
|
+
</div>`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return `<div class="chat-message assistant">${escapeHtml(action.response || '')}</div>`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
window.undoOperation = async function(operationId, btn) {
|
|
271
|
+
btn.disabled = true;
|
|
272
|
+
btn.textContent = '撤销中...';
|
|
273
|
+
try {
|
|
274
|
+
await apiRequest(`${API_BASE}/items/operations/${operationId}/undo`, {
|
|
275
|
+
method: 'POST'
|
|
276
|
+
});
|
|
277
|
+
btn.textContent = '已撤销';
|
|
278
|
+
btn.classList.add('undone');
|
|
279
|
+
if (state.currentSandbox) {
|
|
280
|
+
await loadSandbox(state.currentSandbox.id);
|
|
281
|
+
}
|
|
282
|
+
} catch (error) {
|
|
283
|
+
btn.textContent = '撤销失败';
|
|
284
|
+
btn.disabled = false;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
async function loadDiaries() {
|
|
289
|
+
state.diaries = await apiRequest(`${API_BASE}/diaries`);
|
|
290
|
+
renderDiaries();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function renderDiaries() {
|
|
294
|
+
const timeline = document.getElementById('diary-timeline');
|
|
295
|
+
if (!timeline) return;
|
|
296
|
+
const filtered = applyDiaryFilters(state.diaries);
|
|
297
|
+
|
|
298
|
+
mountDiaryTimeline('diary-timeline', {
|
|
299
|
+
diaries: filtered,
|
|
300
|
+
getSandboxName,
|
|
301
|
+
onConfirm: async (id) => {
|
|
302
|
+
await apiRequest(`${API_BASE}/diaries/${id}/process`, {
|
|
303
|
+
method: 'PUT',
|
|
304
|
+
body: JSON.stringify({ action: 'confirm' }),
|
|
305
|
+
});
|
|
306
|
+
await loadDiaries();
|
|
307
|
+
},
|
|
308
|
+
onIgnore: async (id) => {
|
|
309
|
+
await apiRequest(`${API_BASE}/diaries/${id}/process`, {
|
|
310
|
+
method: 'PUT',
|
|
311
|
+
body: JSON.stringify({ action: 'ignore' }),
|
|
312
|
+
});
|
|
313
|
+
await loadDiaries();
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function getSandboxName(id) {
|
|
319
|
+
const s = state.sandboxes.find(s => s.id === id);
|
|
320
|
+
return s ? s.name : id;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function updateSandboxSelect() {
|
|
324
|
+
const diarySelect = document.getElementById('diary-sandbox-select');
|
|
325
|
+
if (diarySelect) {
|
|
326
|
+
diarySelect.innerHTML = '<option value="">选择沙盘(可选)</option>' +
|
|
327
|
+
state.sandboxes.map(s => `<option value="${s.id}">${safeText(s.name)}</option>`).join('');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const changesSelect = document.getElementById('changes-sandbox-filter');
|
|
331
|
+
if (changesSelect) {
|
|
332
|
+
const currentValue = state.changesSandboxFilter || '';
|
|
333
|
+
changesSelect.innerHTML = '<option value="">全部沙盘</option>' +
|
|
334
|
+
state.sandboxes.map(s => `<option value="${s.id}">${safeText(s.name)}</option>`).join('');
|
|
335
|
+
changesSelect.value = currentValue;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function generateSandboxReportMarkdown(sandbox, mode = 'management') {
|
|
340
|
+
const items = sandbox?.items || [];
|
|
341
|
+
const diaries = sandbox?.diaries || [];
|
|
342
|
+
const lines = [];
|
|
343
|
+
lines.push(`# 沙盘汇报:${sandbox?.name || ''}`);
|
|
344
|
+
lines.push('');
|
|
345
|
+
|
|
346
|
+
if (mode === 'management') {
|
|
347
|
+
lines.push(`- 任务总数:${items.length}`);
|
|
348
|
+
lines.push(`- 已完成:${items.filter((i) => i.status === 'done').length}`);
|
|
349
|
+
lines.push(`- 进行中:${items.filter((i) => i.status === 'in_progress').length}`);
|
|
350
|
+
lines.push(`- 待处理:${items.filter((i) => i.status === 'pending').length}`);
|
|
351
|
+
lines.push(`- 关联日记:${diaries.length}`);
|
|
352
|
+
lines.push('');
|
|
353
|
+
lines.push('## 风险与建议');
|
|
354
|
+
const highPending = items.filter((i) => i.priority === 'high' && i.status !== 'done').length;
|
|
355
|
+
lines.push(`- 高优先级未完成任务:${highPending}`);
|
|
356
|
+
lines.push(`- 进行中任务需关注节奏与阻塞,建议本周优先清理高优先级未完成事项。`);
|
|
357
|
+
} else {
|
|
358
|
+
lines.push('## 执行清单');
|
|
359
|
+
for (const item of items) {
|
|
360
|
+
lines.push(`- [${item.status}] ${item.name}${item.assignee ? ` (@${item.assignee})` : ''}`);
|
|
361
|
+
}
|
|
362
|
+
lines.push('');
|
|
363
|
+
lines.push('## 最近日记(最多10条)');
|
|
364
|
+
for (const diary of diaries.slice(0, 10)) {
|
|
365
|
+
lines.push(`- ${new Date(diary.created_at).toLocaleString()}:${diary.content}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return lines.join('\n');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function parseOpJson(value) {
|
|
372
|
+
if (!value || typeof value !== 'string') return null;
|
|
373
|
+
try {
|
|
374
|
+
return JSON.parse(value);
|
|
375
|
+
} catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function getFilteredOperations() {
|
|
381
|
+
return state.operations.filter((op) => {
|
|
382
|
+
const before = parseOpJson(op.data_before);
|
|
383
|
+
const after = parseOpJson(op.data_after);
|
|
384
|
+
const sandboxMatched = !state.changesSandboxFilter || op.sandbox_id === state.changesSandboxFilter;
|
|
385
|
+
const typeMatched = state.changesTypeFilter === 'all' || op.operation_type === state.changesTypeFilter;
|
|
386
|
+
const quickMatched = state.changesQuickFilter === 'all' || isKeyOperation(op, before, after);
|
|
387
|
+
return sandboxMatched && typeMatched && quickMatched;
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function isKeyOperation(op, before, after) {
|
|
392
|
+
const statusChanged = op.operation_type === 'update' && (before?.status ?? '') !== (after?.status ?? '');
|
|
393
|
+
const highPriorityTouched = (before?.priority === 'high') || (after?.priority === 'high');
|
|
394
|
+
return statusChanged || highPriorityTouched;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function formatDiffLabel(key) {
|
|
398
|
+
const labels = {
|
|
399
|
+
name: '名称',
|
|
400
|
+
status: '状态',
|
|
401
|
+
priority: '优先级',
|
|
402
|
+
assignee: '负责人',
|
|
403
|
+
parent_id: '父任务',
|
|
404
|
+
description: '描述',
|
|
405
|
+
};
|
|
406
|
+
return labels[key] || key;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function buildUpdateDiffLines(before, after) {
|
|
410
|
+
const trackedFields = ['name', 'status', 'priority', 'assignee', 'parent_id', 'description'];
|
|
411
|
+
return trackedFields
|
|
412
|
+
.filter((key) => (before?.[key] ?? '') !== (after?.[key] ?? ''))
|
|
413
|
+
.map((key) => {
|
|
414
|
+
const fromValue = before?.[key] ?? '-';
|
|
415
|
+
const toValue = after?.[key] ?? '-';
|
|
416
|
+
return `${formatDiffLabel(key)}:${safeText(String(fromValue))} -> ${safeText(String(toValue))}`;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function generateWeeklySummaryMarkdown(mode = 'management') {
|
|
421
|
+
const filtered = getFilteredOperations();
|
|
422
|
+
const now = new Date();
|
|
423
|
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
424
|
+
const recentOps = filtered.filter((op) => new Date(op.created_at) >= sevenDaysAgo);
|
|
425
|
+
const createCount = recentOps.filter((op) => op.operation_type === 'create').length;
|
|
426
|
+
const updateCount = recentOps.filter((op) => op.operation_type === 'update').length;
|
|
427
|
+
const deleteCount = recentOps.filter((op) => op.operation_type === 'delete').length;
|
|
428
|
+
const sandboxMap = new Map();
|
|
429
|
+
|
|
430
|
+
for (const op of recentOps) {
|
|
431
|
+
const sandboxName = state.sandboxes.find((s) => s.id === op.sandbox_id)?.name || op.sandbox_id;
|
|
432
|
+
sandboxMap.set(sandboxName, (sandboxMap.get(sandboxName) || 0) + 1);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const keyOps = recentOps.filter((op) => {
|
|
436
|
+
const before = parseOpJson(op.data_before);
|
|
437
|
+
const after = parseOpJson(op.data_after);
|
|
438
|
+
return isKeyOperation(op, before, after);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const lines = [];
|
|
442
|
+
lines.push(`# 变化周报摘要(${mode === 'management' ? '管理层版' : '执行层版'})`);
|
|
443
|
+
lines.push('');
|
|
444
|
+
lines.push(`- 时间范围:${sevenDaysAgo.toLocaleDateString()} ~ ${now.toLocaleDateString()}`);
|
|
445
|
+
lines.push(`- 变化总数:${recentOps.length}`);
|
|
446
|
+
lines.push(`- 创建:${createCount} / 更新:${updateCount} / 删除:${deleteCount}`);
|
|
447
|
+
|
|
448
|
+
if (mode === 'management') {
|
|
449
|
+
lines.push(`- 关键变化:${keyOps.length}`);
|
|
450
|
+
lines.push('');
|
|
451
|
+
lines.push('## 管理关注项');
|
|
452
|
+
lines.push(`- 状态变更或高优先级相关变化共 ${keyOps.length} 条。`);
|
|
453
|
+
lines.push('- 若关键变化集中在单一沙盘,建议在下周设置专题跟踪。');
|
|
454
|
+
lines.push('');
|
|
455
|
+
lines.push('## 沙盘变化分布');
|
|
456
|
+
if (sandboxMap.size === 0) {
|
|
457
|
+
lines.push('- 本周暂无变化');
|
|
458
|
+
} else {
|
|
459
|
+
for (const [sandboxName, count] of sandboxMap.entries()) {
|
|
460
|
+
lines.push(`- ${sandboxName}:${count}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
lines.push('');
|
|
464
|
+
lines.push('## 关键变化(最多 8 条)');
|
|
465
|
+
for (const op of keyOps.slice(0, 8)) {
|
|
466
|
+
const sandboxName = state.sandboxes.find((s) => s.id === op.sandbox_id)?.name || op.sandbox_id;
|
|
467
|
+
const before = parseOpJson(op.data_before);
|
|
468
|
+
const after = parseOpJson(op.data_after);
|
|
469
|
+
const taskName = after?.name || before?.name || '-';
|
|
470
|
+
lines.push(`- ${new Date(op.created_at).toLocaleString()} [${sandboxName}] ${op.operation_type} ${taskName}`);
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
lines.push('');
|
|
474
|
+
lines.push('## 执行建议');
|
|
475
|
+
lines.push('- 优先处理高优先级且未完成任务,避免关键路径阻塞。');
|
|
476
|
+
lines.push('- 对近期频繁变更项补充说明,减少反复沟通。');
|
|
477
|
+
lines.push('');
|
|
478
|
+
lines.push('## 任务变化明细(最多 12 条)');
|
|
479
|
+
for (const op of recentOps.slice(0, 12)) {
|
|
480
|
+
const sandboxName = state.sandboxes.find((s) => s.id === op.sandbox_id)?.name || op.sandbox_id;
|
|
481
|
+
const before = parseOpJson(op.data_before);
|
|
482
|
+
const after = parseOpJson(op.data_after);
|
|
483
|
+
const taskName = after?.name || before?.name || '-';
|
|
484
|
+
const diffLines = op.operation_type === 'update' ? buildUpdateDiffLines(before, after) : [];
|
|
485
|
+
lines.push(`- ${new Date(op.created_at).toLocaleString()} [${sandboxName}] ${op.operation_type} ${taskName}`);
|
|
486
|
+
if (diffLines.length > 0) {
|
|
487
|
+
for (const line of diffLines.slice(0, 3)) {
|
|
488
|
+
lines.push(` - ${line}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return lines.join('\n');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function renderChanges() {
|
|
497
|
+
const container = document.getElementById('changes-list');
|
|
498
|
+
if (!container) return;
|
|
499
|
+
|
|
500
|
+
const filtered = getFilteredOperations();
|
|
501
|
+
|
|
502
|
+
if (!filtered.length) {
|
|
503
|
+
container.innerHTML = '<div class="empty-state"><p>暂无变化记录</p></div>';
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const groups = new Map();
|
|
507
|
+
for (const op of filtered) {
|
|
508
|
+
const key = new Date(op.created_at).toLocaleDateString();
|
|
509
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
510
|
+
groups.get(key).push(op);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
container.innerHTML = Array.from(groups.entries()).map(([date, ops]) => {
|
|
514
|
+
const items = ops.map((op) => {
|
|
515
|
+
const before = parseOpJson(op.data_before);
|
|
516
|
+
const after = parseOpJson(op.data_after);
|
|
517
|
+
const sandboxName = state.sandboxes.find((s) => s.id === op.sandbox_id)?.name || op.sandbox_id;
|
|
518
|
+
let detail = '';
|
|
519
|
+
let diffLinesHtml = '';
|
|
520
|
+
|
|
521
|
+
if (op.operation_type === 'create') {
|
|
522
|
+
detail = `创建任务:${safeText(after?.name || '-')}`;
|
|
523
|
+
} else if (op.operation_type === 'update') {
|
|
524
|
+
detail = `更新任务:${safeText(after?.name || before?.name || '-')}`;
|
|
525
|
+
const diffLines = buildUpdateDiffLines(before, after);
|
|
526
|
+
if (diffLines.length > 0) {
|
|
527
|
+
diffLinesHtml = `<div class="change-diff-list">${diffLines.map((line) => `<div class="change-diff-line">${line}</div>`).join('')}</div>`;
|
|
528
|
+
}
|
|
529
|
+
} else if (op.operation_type === 'delete') {
|
|
530
|
+
detail = `删除任务:${safeText(before?.name || '-')}`;
|
|
531
|
+
}
|
|
532
|
+
return `
|
|
533
|
+
<div class="change-item">
|
|
534
|
+
<div class="change-meta">
|
|
535
|
+
<span class="change-type ${safeText(op.operation_type)}">${safeText(op.operation_type)}</span>
|
|
536
|
+
<span>${safeText(sandboxName)}</span>
|
|
537
|
+
<span>${new Date(op.created_at).toLocaleTimeString()}</span>
|
|
538
|
+
</div>
|
|
539
|
+
<div class="change-detail">${detail}</div>
|
|
540
|
+
${diffLinesHtml}
|
|
541
|
+
</div>
|
|
542
|
+
`;
|
|
543
|
+
}).join('');
|
|
544
|
+
|
|
545
|
+
return `
|
|
546
|
+
<div class="change-date-group">
|
|
547
|
+
<h4 class="change-date-title">${safeText(date)}</h4>
|
|
548
|
+
${items}
|
|
549
|
+
</div>
|
|
550
|
+
`;
|
|
551
|
+
}).join('');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function loadChanges() {
|
|
555
|
+
const query = new URLSearchParams();
|
|
556
|
+
query.set('limit', '80');
|
|
557
|
+
if (state.changesSandboxFilter) {
|
|
558
|
+
query.set('sandbox_id', state.changesSandboxFilter);
|
|
559
|
+
}
|
|
560
|
+
state.operations = await apiRequest(`${API_BASE}/items/operations/recent?${query.toString()}`);
|
|
561
|
+
renderChanges();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function loadSettings() {
|
|
565
|
+
state.settings = await apiRequest(`${API_BASE}/settings`);
|
|
566
|
+
document.getElementById('setting-api-url').value = state.settings.api_url || '';
|
|
567
|
+
const apiKeyInput = document.getElementById('setting-api-key');
|
|
568
|
+
apiKeyInput.value = '';
|
|
569
|
+
apiKeyInput.placeholder = state.settings.has_api_key ? '已配置,留空表示不修改' : 'sk-...';
|
|
570
|
+
document.getElementById('setting-model').value = state.settings.model || '';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function loadChats() {
|
|
574
|
+
const messages = document.getElementById('chat-messages');
|
|
575
|
+
if (!messages) return;
|
|
576
|
+
|
|
577
|
+
const chats = await apiRequest(`${API_BASE}/chats`);
|
|
578
|
+
mountHtmlList('chat-messages', chats.map((chat) => renderChatEntry(chat, { safeText, renderAIActionMessage })));
|
|
579
|
+
messages.scrollTop = messages.scrollHeight;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function showPage(pageId) {
|
|
583
|
+
document.querySelectorAll('.page').forEach(p => p.classList.add('hidden'));
|
|
584
|
+
const page = document.getElementById(`page-${pageId}`);
|
|
585
|
+
if (page) {
|
|
586
|
+
page.classList.remove('hidden');
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
document.querySelectorAll('.nav-list a').forEach(a => a.classList.remove('active'));
|
|
590
|
+
const nav = document.querySelector(`[data-nav="${pageId}"]`);
|
|
591
|
+
if (nav) nav.classList.add('active');
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function editWorkItem(id) {
|
|
595
|
+
const item = state.currentSandbox.items.find(i => i.id === id);
|
|
596
|
+
if (!item) return;
|
|
597
|
+
|
|
598
|
+
document.getElementById('item-dialog-title').textContent = '编辑工作项';
|
|
599
|
+
document.getElementById('new-item-name').value = item.name;
|
|
600
|
+
document.getElementById('new-item-desc').value = item.description;
|
|
601
|
+
document.getElementById('new-item-assignee').value = item.assignee;
|
|
602
|
+
document.getElementById('new-item-status').value = item.status;
|
|
603
|
+
document.getElementById('new-item-priority').value = item.priority;
|
|
604
|
+
|
|
605
|
+
document.getElementById('item-dialog').dataset.editId = id;
|
|
606
|
+
document.getElementById('item-dialog').showModal();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function initApp() {
|
|
610
|
+
const hash = window.location.hash;
|
|
611
|
+
|
|
612
|
+
document.querySelectorAll('.nav-list a').forEach(link => {
|
|
613
|
+
link.addEventListener('click', (e) => {
|
|
614
|
+
e.preventDefault();
|
|
615
|
+
window.location.hash = link.getAttribute('href').replace('#', '') || '/';
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
document.getElementById('add-sandbox-btn')?.addEventListener('click', () => {
|
|
620
|
+
document.getElementById('sandbox-dialog').showModal();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
document.getElementById('cancel-sandbox-btn')?.addEventListener('click', () => {
|
|
624
|
+
document.getElementById('sandbox-dialog').close();
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
document.getElementById('sandbox-dialog').addEventListener('submit', async (e) => {
|
|
628
|
+
e.preventDefault();
|
|
629
|
+
const name = document.getElementById('new-sandbox-name').value;
|
|
630
|
+
const description = document.getElementById('new-sandbox-desc').value;
|
|
631
|
+
const dialog = document.getElementById('sandbox-dialog');
|
|
632
|
+
|
|
633
|
+
try {
|
|
634
|
+
await apiRequest(`${API_BASE}/sandboxes`, {
|
|
635
|
+
method: 'POST',
|
|
636
|
+
body: JSON.stringify({ name, description }),
|
|
637
|
+
});
|
|
638
|
+
dialog.close();
|
|
639
|
+
document.getElementById('new-sandbox-name').value = '';
|
|
640
|
+
document.getElementById('new-sandbox-desc').value = '';
|
|
641
|
+
await loadSandboxes();
|
|
642
|
+
} catch (error) {
|
|
643
|
+
alert('创建失败: ' + error.message);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
document.getElementById('add-item-btn')?.addEventListener('click', () => {
|
|
648
|
+
document.getElementById('item-dialog-title').textContent = '添加任务';
|
|
649
|
+
document.getElementById('item-dialog').dataset.editId = '';
|
|
650
|
+
document.getElementById('new-item-name').value = '';
|
|
651
|
+
document.getElementById('new-item-desc').value = '';
|
|
652
|
+
document.getElementById('new-item-assignee').value = '';
|
|
653
|
+
document.getElementById('new-item-status').value = 'pending';
|
|
654
|
+
document.getElementById('new-item-priority').value = 'medium';
|
|
655
|
+
document.getElementById('new-item-parent').value = '';
|
|
656
|
+
document.getElementById('item-dialog').showModal();
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
document.getElementById('cancel-item-btn')?.addEventListener('click', () => {
|
|
660
|
+
document.getElementById('item-dialog').close();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
document.getElementById('confirm-item-btn')?.addEventListener('click', async () => {
|
|
664
|
+
const dialog = document.getElementById('item-dialog');
|
|
665
|
+
const editId = dialog.dataset.editId || null;
|
|
666
|
+
const isNewItem = !editId;
|
|
667
|
+
|
|
668
|
+
const data = {
|
|
669
|
+
name: document.getElementById('new-item-name').value,
|
|
670
|
+
description: document.getElementById('new-item-desc').value,
|
|
671
|
+
assignee: document.getElementById('new-item-assignee').value,
|
|
672
|
+
status: document.getElementById('new-item-status').value,
|
|
673
|
+
priority: document.getElementById('new-item-priority').value,
|
|
674
|
+
parent_id: document.getElementById('new-item-parent').value || null,
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
if (!data.name) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const btn = document.getElementById('confirm-item-btn');
|
|
682
|
+
const originalText = btn.textContent;
|
|
683
|
+
setButtonState(btn, { disabled: true, text: '保存中...' });
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
if (editId) {
|
|
687
|
+
await apiRequest(`${API_BASE}/items/${editId}`, {
|
|
688
|
+
method: 'PUT',
|
|
689
|
+
body: JSON.stringify(data),
|
|
690
|
+
});
|
|
691
|
+
} else {
|
|
692
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/items`, {
|
|
693
|
+
method: 'POST',
|
|
694
|
+
body: JSON.stringify(data),
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
dialog.close();
|
|
700
|
+
expandedNodes.clear();
|
|
701
|
+
await loadSandbox(state.currentSandbox.id);
|
|
702
|
+
} catch (error) {
|
|
703
|
+
setButtonState(btn, { disabled: false, text: originalText || '保存' });
|
|
704
|
+
} finally {
|
|
705
|
+
setButtonState(btn, { disabled: false, text: originalText || '保存' });
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
document.getElementById('rollback-btn')?.addEventListener('click', async () => {
|
|
710
|
+
if (!state.currentSandbox?.items?.length) return;
|
|
711
|
+
const lastItem = state.currentSandbox.items[state.currentSandbox.items.length - 1];
|
|
712
|
+
await apiRequest(`${API_BASE}/items/${lastItem.id}/rollback`, { method: 'POST' });
|
|
713
|
+
await loadSandbox(state.currentSandbox.id);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
document.getElementById('send-btn')?.addEventListener('click', async () => {
|
|
717
|
+
const input = document.getElementById('chat-input');
|
|
718
|
+
const content = input.value.trim();
|
|
719
|
+
if (!content) return;
|
|
720
|
+
|
|
721
|
+
const messages = document.getElementById('chat-messages');
|
|
722
|
+
messages.insertAdjacentHTML('beforeend', `<div class="chat-message user">${escapeHtml(content)}</div>`);
|
|
723
|
+
const loadingMessage = appendLoadingMessage(messages);
|
|
724
|
+
messages.scrollTop = messages.scrollHeight;
|
|
725
|
+
input.value = '';
|
|
726
|
+
|
|
727
|
+
const btn = document.getElementById('send-btn');
|
|
728
|
+
setButtonState(btn, { disabled: true });
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
await apiRequest(`${API_BASE}/chats`, {
|
|
732
|
+
method: 'POST',
|
|
733
|
+
body: JSON.stringify({ content }),
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
loadingMessage?.remove();
|
|
737
|
+
await loadChats();
|
|
738
|
+
} catch (error) {
|
|
739
|
+
loadingMessage?.remove();
|
|
740
|
+
messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant error">❌ 错误: ${escapeHtml(error.message)}</div>`);
|
|
741
|
+
messages.scrollTop = messages.scrollHeight;
|
|
742
|
+
} finally {
|
|
743
|
+
setButtonState(btn, { disabled: false });
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
document.getElementById('chat-input')?.addEventListener('keypress', (e) => {
|
|
748
|
+
if (e.key === 'Enter') {
|
|
749
|
+
document.getElementById('send-btn').click();
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
document.getElementById('sandbox-send-btn')?.addEventListener('click', async () => {
|
|
754
|
+
const input = document.getElementById('sandbox-chat-input');
|
|
755
|
+
const content = input.value.trim();
|
|
756
|
+
if (!content || !state.currentSandbox) return;
|
|
757
|
+
|
|
758
|
+
const btn = document.getElementById('sandbox-send-btn');
|
|
759
|
+
const messages = document.getElementById('sandbox-chat-messages');
|
|
760
|
+
|
|
761
|
+
messages.insertAdjacentHTML('beforeend', `<div class="chat-message user">${escapeHtml(content)}</div>`);
|
|
762
|
+
const loadingMessage = appendLoadingMessage(messages);
|
|
763
|
+
messages.scrollTop = messages.scrollHeight;
|
|
764
|
+
input.value = '';
|
|
765
|
+
|
|
766
|
+
const originalText = btn.textContent;
|
|
767
|
+
setButtonState(btn, { disabled: true, text: '思考中' });
|
|
768
|
+
|
|
769
|
+
try {
|
|
770
|
+
const history = state.chats
|
|
771
|
+
.filter(c => c.role)
|
|
772
|
+
.slice(-10)
|
|
773
|
+
.map(c => ({ role: c.role, content: typeof c.content === 'string' ? c.content : JSON.stringify(c.content) }));
|
|
774
|
+
|
|
775
|
+
const result = await apiRequest(`${API_BASE}/chats/react`, {
|
|
776
|
+
method: 'POST',
|
|
777
|
+
body: JSON.stringify({
|
|
778
|
+
content,
|
|
779
|
+
sandbox_id: state.currentSandbox.id,
|
|
780
|
+
history,
|
|
781
|
+
pending_action: state.pendingAction
|
|
782
|
+
}),
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
console.log('Chat result:', result);
|
|
786
|
+
|
|
787
|
+
loadingMessage?.remove();
|
|
788
|
+
|
|
789
|
+
await handleAIAction(result.action, state.currentSandbox.id);
|
|
790
|
+
await loadSandbox(state.currentSandbox.id);
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.error('发送消息失败:', error);
|
|
793
|
+
loadingMessage?.remove();
|
|
794
|
+
messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant error">❌ 错误: ${escapeHtml(error.message)}</div>`);
|
|
795
|
+
messages.scrollTop = messages.scrollHeight;
|
|
796
|
+
} finally {
|
|
797
|
+
setButtonState(btn, { disabled: false, text: originalText || '发送' });
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
async function handleAIAction(action, sandboxId) {
|
|
802
|
+
if (!action) return;
|
|
803
|
+
|
|
804
|
+
const messages = document.getElementById('sandbox-chat-messages');
|
|
805
|
+
if (!messages) return;
|
|
806
|
+
|
|
807
|
+
const actionType = action.action;
|
|
808
|
+
|
|
809
|
+
if (actionType === 'response' || actionType === 'clarify') {
|
|
810
|
+
messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant">${safeText(action.response || action.observation || '')}</div>`);
|
|
811
|
+
messages.scrollTop = messages.scrollHeight;
|
|
812
|
+
|
|
813
|
+
if (actionType === 'clarify') {
|
|
814
|
+
if (action.pending_action) {
|
|
815
|
+
state.pendingAction = action.pending_action;
|
|
816
|
+
} else if (action.response && action.response.includes('创建')) {
|
|
817
|
+
const lastUserMsg = state.chats.filter(c => c.role === 'user').pop();
|
|
818
|
+
if (lastUserMsg && lastUserMsg.content) {
|
|
819
|
+
state.pendingAction = {
|
|
820
|
+
action_type: 'create_work_item',
|
|
821
|
+
action_input: { name: '待创建任务' }
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
} else if (actionType === 'response') {
|
|
826
|
+
state.pendingAction = null;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
else if (actionType === 'confirm' && action.confirm_items) {
|
|
830
|
+
let html = '<div class="confirm-panel">';
|
|
831
|
+
html += `<div class="ai-thought">💭 ${safeText(action.thought || '')}</div>`;
|
|
832
|
+
html += '<h4>请确认以下操作:</h4>';
|
|
833
|
+
|
|
834
|
+
for (const item of action.confirm_items) {
|
|
835
|
+
const actionText = {
|
|
836
|
+
create_work_item: '创建任务',
|
|
837
|
+
update_work_item: '更新任务',
|
|
838
|
+
delete_work_item: '删除任务',
|
|
839
|
+
move_work_item: '移动任务'
|
|
840
|
+
};
|
|
841
|
+
html += `<div class="confirm-item">
|
|
842
|
+
<span class="confirm-action">${safeText(actionText[item.type] || item.type)}</span>
|
|
843
|
+
<span class="confirm-name">${safeText(item.name)}</span>
|
|
844
|
+
${item.description ? `<span class="confirm-desc">${safeText(item.description)}</span>` : ''}
|
|
845
|
+
</div>`;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
html += '<div class="confirm-actions">';
|
|
849
|
+
html += '<button class="btn btn-secondary" id="cancel-confirm-btn">取消</button>';
|
|
850
|
+
html += '<button class="btn btn-primary" id="execute-confirm-btn">确认执行</button>';
|
|
851
|
+
html += '</div></div>';
|
|
852
|
+
|
|
853
|
+
messages.insertAdjacentHTML('beforeend', html);
|
|
854
|
+
messages.scrollTop = messages.scrollHeight;
|
|
855
|
+
|
|
856
|
+
document.getElementById('cancel-confirm-btn')?.addEventListener('click', () => {
|
|
857
|
+
document.querySelector('.confirm-panel')?.remove();
|
|
858
|
+
messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant">已取消操作</div>`);
|
|
859
|
+
messages.scrollTop = messages.scrollHeight;
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
document.getElementById('execute-confirm-btn')?.addEventListener('click', async () => {
|
|
863
|
+
try {
|
|
864
|
+
const confirmData = action.confirm_items.map(item => ({
|
|
865
|
+
type: item.type,
|
|
866
|
+
name: item.name,
|
|
867
|
+
description: item.description,
|
|
868
|
+
params: item.params
|
|
869
|
+
}));
|
|
870
|
+
|
|
871
|
+
const confirmResult = await apiRequest(`${API_BASE}/chats/confirm`, {
|
|
872
|
+
method: 'POST',
|
|
873
|
+
body: JSON.stringify({
|
|
874
|
+
sandbox_id: sandboxId,
|
|
875
|
+
confirm_items: confirmData
|
|
876
|
+
}),
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
document.querySelector('.confirm-panel')?.remove();
|
|
880
|
+
await loadSandbox(sandboxId);
|
|
881
|
+
const results = Array.isArray(confirmResult?.results) ? confirmResult.results : [];
|
|
882
|
+
const successCount = results.filter((item) => item.success).length;
|
|
883
|
+
const totalCount = results.length;
|
|
884
|
+
const failCount = totalCount - successCount;
|
|
885
|
+
const statusText = totalCount === 0
|
|
886
|
+
? '未执行任何操作'
|
|
887
|
+
: `执行完成:成功 ${successCount}/${totalCount}${failCount > 0 ? `,失败 ${failCount}` : ''}`;
|
|
888
|
+
const statusIcon = failCount === 0 ? '✅' : successCount === 0 ? '❌' : '⚠️';
|
|
889
|
+
messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant">${statusIcon} ${safeText(statusText)}</div>`);
|
|
890
|
+
messages.scrollTop = messages.scrollHeight;
|
|
891
|
+
} catch (error) {
|
|
892
|
+
alert('执行失败: ' + error.message);
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
else if (actionType === 'done') {
|
|
897
|
+
messages.insertAdjacentHTML('beforeend', renderAIActionMessage(action));
|
|
898
|
+
messages.scrollTop = messages.scrollHeight;
|
|
899
|
+
state.pendingAction = null;
|
|
900
|
+
}
|
|
901
|
+
else if (actionType === 'stop') {
|
|
902
|
+
messages.insertAdjacentHTML('beforeend', `<div class="chat-message assistant">${safeText(action.observation || '已取消操作')}</div>`);
|
|
903
|
+
messages.scrollTop = messages.scrollHeight;
|
|
904
|
+
state.pendingAction = null;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
document.getElementById('sandbox-chat-input')?.addEventListener('keypress', (e) => {
|
|
909
|
+
if (e.key === 'Enter') {
|
|
910
|
+
document.getElementById('sandbox-send-btn').click();
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
document.getElementById('save-diary-btn')?.addEventListener('click', async () => {
|
|
915
|
+
const sandboxId = document.getElementById('diary-sandbox-select').value || null;
|
|
916
|
+
const content = document.getElementById('diary-content').value.trim();
|
|
917
|
+
if (!content) return;
|
|
918
|
+
|
|
919
|
+
await apiRequest(`${API_BASE}/diaries`, {
|
|
920
|
+
method: 'POST',
|
|
921
|
+
body: JSON.stringify({ sandbox_id: sandboxId, content }),
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
document.getElementById('diary-content').value = '';
|
|
925
|
+
await loadDiaries();
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
document.getElementById('save-settings-btn')?.addEventListener('click', async () => {
|
|
929
|
+
const api_url = document.getElementById('setting-api-url').value;
|
|
930
|
+
const api_key = document.getElementById('setting-api-key').value;
|
|
931
|
+
const model = document.getElementById('setting-model').value;
|
|
932
|
+
|
|
933
|
+
await apiRequest(`${API_BASE}/settings/api_url`, {
|
|
934
|
+
method: 'PUT',
|
|
935
|
+
body: JSON.stringify({ value: api_url }),
|
|
936
|
+
});
|
|
937
|
+
if (api_key) {
|
|
938
|
+
await apiRequest(`${API_BASE}/settings/api_key`, {
|
|
939
|
+
method: 'PUT',
|
|
940
|
+
body: JSON.stringify({ value: api_key }),
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
await apiRequest(`${API_BASE}/settings/model`, {
|
|
944
|
+
method: 'PUT',
|
|
945
|
+
body: JSON.stringify({ value: model }),
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
alert('设置已保存');
|
|
949
|
+
await loadSettings();
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
document.getElementById('work-item-search')?.addEventListener('input', (e) => {
|
|
953
|
+
state.workItemSearch = e.target.value || '';
|
|
954
|
+
renderWorkTree();
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
document.getElementById('work-item-status-filter')?.addEventListener('change', (e) => {
|
|
958
|
+
state.workItemStatusFilter = e.target.value || 'all';
|
|
959
|
+
renderWorkTree();
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
document.getElementById('diary-search')?.addEventListener('input', (e) => {
|
|
963
|
+
state.diarySearch = e.target.value || '';
|
|
964
|
+
renderDiaries();
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
document.getElementById('diary-processed-filter')?.addEventListener('change', (e) => {
|
|
968
|
+
state.diaryProcessedFilter = e.target.value || 'all';
|
|
969
|
+
renderDiaries();
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
document.getElementById('generate-report-btn')?.addEventListener('click', () => {
|
|
973
|
+
if (!state.currentSandbox) return;
|
|
974
|
+
const mode = document.getElementById('report-template')?.value || 'management';
|
|
975
|
+
const report = generateSandboxReportMarkdown(state.currentSandbox, mode);
|
|
976
|
+
document.getElementById('report-content').value = report;
|
|
977
|
+
document.getElementById('report-dialog').showModal();
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
document.getElementById('report-template')?.addEventListener('change', (e) => {
|
|
981
|
+
if (!state.currentSandbox) return;
|
|
982
|
+
const mode = e.target.value || 'management';
|
|
983
|
+
document.getElementById('report-content').value = generateSandboxReportMarkdown(state.currentSandbox, mode);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
document.getElementById('close-report-btn')?.addEventListener('click', () => {
|
|
987
|
+
document.getElementById('report-dialog').close();
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
document.getElementById('copy-report-btn')?.addEventListener('click', async () => {
|
|
991
|
+
const content = document.getElementById('report-content').value;
|
|
992
|
+
await navigator.clipboard.writeText(content);
|
|
993
|
+
alert('汇报已复制');
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
document.getElementById('generate-insight-btn')?.addEventListener('click', async () => {
|
|
997
|
+
if (!state.currentSandbox) return;
|
|
998
|
+
const output = document.getElementById('sandbox-insight-output');
|
|
999
|
+
output.classList.remove('hidden');
|
|
1000
|
+
output.textContent = '生成中...';
|
|
1001
|
+
try {
|
|
1002
|
+
const result = await apiRequest(`${API_BASE}/chats/sandbox/${state.currentSandbox.id}/insight`);
|
|
1003
|
+
output.textContent = result.insight;
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
output.textContent = `生成失败:${error.message}`;
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
document.getElementById('changes-sandbox-filter')?.addEventListener('change', async (e) => {
|
|
1010
|
+
state.changesSandboxFilter = e.target.value || '';
|
|
1011
|
+
await loadChanges();
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
document.getElementById('changes-type-filter')?.addEventListener('change', (e) => {
|
|
1015
|
+
state.changesTypeFilter = e.target.value || 'all';
|
|
1016
|
+
renderChanges();
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
document.getElementById('changes-quick-filter')?.addEventListener('change', (e) => {
|
|
1020
|
+
state.changesQuickFilter = e.target.value || 'all';
|
|
1021
|
+
renderChanges();
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
document.getElementById('refresh-changes-btn')?.addEventListener('click', async () => {
|
|
1025
|
+
await loadChanges();
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
document.getElementById('export-weekly-summary-btn')?.addEventListener('click', () => {
|
|
1029
|
+
const mode = document.getElementById('weekly-summary-template')?.value || 'management';
|
|
1030
|
+
const content = generateWeeklySummaryMarkdown(mode);
|
|
1031
|
+
document.getElementById('weekly-summary-content').value = content;
|
|
1032
|
+
document.getElementById('weekly-summary-dialog').showModal();
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
document.getElementById('weekly-summary-template')?.addEventListener('change', (e) => {
|
|
1036
|
+
const mode = e.target.value || 'management';
|
|
1037
|
+
document.getElementById('weekly-summary-content').value = generateWeeklySummaryMarkdown(mode);
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
document.getElementById('close-weekly-summary-btn')?.addEventListener('click', () => {
|
|
1041
|
+
document.getElementById('weekly-summary-dialog').close();
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
document.getElementById('copy-weekly-summary-btn')?.addEventListener('click', async () => {
|
|
1045
|
+
const content = document.getElementById('weekly-summary-content').value;
|
|
1046
|
+
await navigator.clipboard.writeText(content);
|
|
1047
|
+
alert('周报摘要已复制');
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
window.addEventListener('hashchange', handleRoute);
|
|
1051
|
+
handleRoute();
|
|
1052
|
+
|
|
1053
|
+
async function handleRoute() {
|
|
1054
|
+
const hash = window.location.hash.slice(1) || '/';
|
|
1055
|
+
|
|
1056
|
+
if (hash === '/') {
|
|
1057
|
+
showPage('home');
|
|
1058
|
+
await loadChats();
|
|
1059
|
+
} else if (hash === '/sandboxes') {
|
|
1060
|
+
showPage('sandboxes');
|
|
1061
|
+
await loadSandboxes();
|
|
1062
|
+
} else if (hash.startsWith('/sandbox/')) {
|
|
1063
|
+
const id = hash.split('/')[2];
|
|
1064
|
+
showPage('sandbox-detail');
|
|
1065
|
+
await loadSandbox(id);
|
|
1066
|
+
} else if (hash === '/diaries') {
|
|
1067
|
+
showPage('diaries');
|
|
1068
|
+
await loadDiaries();
|
|
1069
|
+
await loadSandboxes();
|
|
1070
|
+
} else if (hash === '/changes') {
|
|
1071
|
+
showPage('changes');
|
|
1072
|
+
await loadSandboxes();
|
|
1073
|
+
await loadChanges();
|
|
1074
|
+
} else if (hash === '/settings') {
|
|
1075
|
+
showPage('settings');
|
|
1076
|
+
await loadSettings();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
document.addEventListener('DOMContentLoaded', initApp);
|