@qnote/q-ai-note 1.0.12 → 1.0.14
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/dist/server/api/aserRuntime.d.ts +3 -0
- package/dist/server/api/aserRuntime.d.ts.map +1 -0
- package/dist/server/api/aserRuntime.js +401 -0
- package/dist/server/api/aserRuntime.js.map +1 -0
- package/dist/server/api/nodeEntities.d.ts.map +1 -1
- package/dist/server/api/nodeEntities.js +2 -0
- package/dist/server/api/nodeEntities.js.map +1 -1
- package/dist/server/api/sandbox.d.ts.map +1 -1
- package/dist/server/api/sandbox.js +228 -1
- package/dist/server/api/sandbox.js.map +1 -1
- package/dist/server/api/workItem.d.ts.map +1 -1
- package/dist/server/api/workItem.js +145 -0
- package/dist/server/api/workItem.js.map +1 -1
- package/dist/server/aserRuntimeStore.d.ts +199 -0
- package/dist/server/aserRuntimeStore.d.ts.map +1 -0
- package/dist/server/aserRuntimeStore.js +826 -0
- package/dist/server/aserRuntimeStore.js.map +1 -0
- package/dist/server/db.d.ts.map +1 -1
- package/dist/server/db.js +9 -0
- package/dist/server/db.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/nodeEntitiesStore.d.ts +3 -0
- package/dist/server/nodeEntitiesStore.d.ts.map +1 -1
- package/dist/server/nodeEntitiesStore.js +5 -3
- package/dist/server/nodeEntitiesStore.js.map +1 -1
- package/dist/web/app.js +1239 -46
- package/dist/web/aserRuntimeView.js +2152 -0
- package/dist/web/index.html +356 -2
- package/dist/web/styles.css +1548 -113
- package/dist/web/vueRenderers.js +30 -3
- package/package.json +2 -1
package/dist/web/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { API_BASE, apiRequest, escapeHtml, safeText, appendLoadingMessage, setButtonState } from './shared.js';
|
|
2
2
|
import { renderChatEntry } from './chatView.js';
|
|
3
3
|
import { mountSandboxGrid, mountHtmlList, mountDiaryTimeline, mountWorkTree } from './vueRenderers.js';
|
|
4
|
+
import { createAserRuntimeView } from './aserRuntimeView.js';
|
|
4
5
|
|
|
5
6
|
const state = {
|
|
6
7
|
sandboxes: [],
|
|
@@ -36,8 +37,12 @@ const state = {
|
|
|
36
37
|
workTreeViewMode: 'full',
|
|
37
38
|
workItemElementPreviewMode: 'none',
|
|
38
39
|
workItemShowAssignee: false,
|
|
40
|
+
sandboxSortMode: 'created_asc',
|
|
39
41
|
workItemSearch: '',
|
|
40
42
|
workItemStatusFilter: 'all',
|
|
43
|
+
taskBoardPeriod: 'week',
|
|
44
|
+
taskBoardOwnerName: '',
|
|
45
|
+
taskBoardData: null,
|
|
41
46
|
diarySearch: '',
|
|
42
47
|
diarySandboxFilter: '',
|
|
43
48
|
diaryProcessedFilter: 'all',
|
|
@@ -46,6 +51,16 @@ const state = {
|
|
|
46
51
|
changesTypeFilter: 'all',
|
|
47
52
|
changesQuickFilter: 'all',
|
|
48
53
|
workTreeExpandInitialized: false,
|
|
54
|
+
agentClub: [],
|
|
55
|
+
teams: [],
|
|
56
|
+
aserProjectTeamAssignments: {},
|
|
57
|
+
opencodeRunnerProjects: [],
|
|
58
|
+
opencodeRunnerSelectedProjectId: '',
|
|
59
|
+
opencodeRunnerSelectedTeamId: '',
|
|
60
|
+
opencodeRunnerTasks: [],
|
|
61
|
+
opencodeRunnerTimeline: [],
|
|
62
|
+
opencodeRunnerRunning: false,
|
|
63
|
+
opencodeRunnerPlanned: false,
|
|
49
64
|
};
|
|
50
65
|
|
|
51
66
|
const expandedNodes = new Set();
|
|
@@ -53,8 +68,14 @@ let quickChatPopover = null;
|
|
|
53
68
|
let sandboxActionHandler = null;
|
|
54
69
|
let sandboxEscLocked = false;
|
|
55
70
|
let resizeRenderTimer = null;
|
|
71
|
+
let aserRuntimeView = null;
|
|
56
72
|
const WORK_ITEM_SHOW_ASSIGNEE_STORAGE_KEY = 'q-ai-note.work-item.show-assignee';
|
|
57
73
|
const WORK_TREE_VIEW_MODE_STORAGE_KEY = 'q-ai-note.work-tree.view-mode';
|
|
74
|
+
const SANDBOX_SORT_MODE_STORAGE_KEY = 'q-ai-note.sandbox.sort-mode';
|
|
75
|
+
const TASK_BOARD_OWNER_STORAGE_KEY = 'q-ai-note.task-board.owner-name';
|
|
76
|
+
const AGENT_CLUB_STORAGE_KEY = 'q-ai-note.ai-engineering.agent-club';
|
|
77
|
+
const TEAMS_STORAGE_KEY = 'q-ai-note.ai-engineering.teams';
|
|
78
|
+
const ASER_PROJECT_TEAM_ASSIGNMENTS_STORAGE_KEY = 'q-ai-note.ai-engineering.aser-project-team-assignments';
|
|
58
79
|
|
|
59
80
|
function loadWorkItemAssigneePreference() {
|
|
60
81
|
try {
|
|
@@ -96,6 +117,558 @@ function persistWorkTreeViewModePreference() {
|
|
|
96
117
|
}
|
|
97
118
|
}
|
|
98
119
|
|
|
120
|
+
function loadSandboxSortModePreference() {
|
|
121
|
+
try {
|
|
122
|
+
const raw = String(window.localStorage.getItem(SANDBOX_SORT_MODE_STORAGE_KEY) || '').trim();
|
|
123
|
+
if (!raw) return;
|
|
124
|
+
if (raw === 'created_asc' || raw === 'created_desc' || raw === 'updated_desc' || raw === 'name_asc') {
|
|
125
|
+
state.sandboxSortMode = raw;
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Ignore storage failures in restricted environments.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function persistSandboxSortModePreference() {
|
|
133
|
+
try {
|
|
134
|
+
window.localStorage.setItem(SANDBOX_SORT_MODE_STORAGE_KEY, state.sandboxSortMode || 'created_asc');
|
|
135
|
+
} catch {
|
|
136
|
+
// Ignore storage failures in restricted environments.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function loadTaskBoardOwnerNamePreference() {
|
|
141
|
+
try {
|
|
142
|
+
state.taskBoardOwnerName = String(window.localStorage.getItem(TASK_BOARD_OWNER_STORAGE_KEY) || '').trim();
|
|
143
|
+
} catch {
|
|
144
|
+
state.taskBoardOwnerName = '';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function persistTaskBoardOwnerNamePreference() {
|
|
149
|
+
try {
|
|
150
|
+
window.localStorage.setItem(TASK_BOARD_OWNER_STORAGE_KEY, state.taskBoardOwnerName || '');
|
|
151
|
+
} catch {
|
|
152
|
+
// Ignore storage failures in restricted environments.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function safeParseJson(rawValue, fallback) {
|
|
157
|
+
if (!rawValue) return fallback;
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(rawValue);
|
|
160
|
+
} catch {
|
|
161
|
+
return fallback;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseLineList(rawValue) {
|
|
166
|
+
return String(rawValue || '')
|
|
167
|
+
.split('\n')
|
|
168
|
+
.map((value) => value.trim())
|
|
169
|
+
.filter(Boolean);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseSkillRows(rawValue) {
|
|
173
|
+
return parseLineList(rawValue).map((row) => {
|
|
174
|
+
const [skillPart, levelPart = ''] = row.split(':');
|
|
175
|
+
const skill = String(skillPart || '').trim();
|
|
176
|
+
const level = String(levelPart || '').trim();
|
|
177
|
+
return {
|
|
178
|
+
skill,
|
|
179
|
+
level: level || 'intermediate',
|
|
180
|
+
};
|
|
181
|
+
}).filter((row) => Boolean(row.skill));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeAgentRecord(rawAgent = {}) {
|
|
185
|
+
const id = String(rawAgent.id || '').trim() || `agent-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
|
186
|
+
const roleType = String(rawAgent.role_type || rawAgent.role || '').trim() || 'generalist';
|
|
187
|
+
const legacySkill = String(rawAgent.skill || '').trim();
|
|
188
|
+
const rawSkills = Array.isArray(rawAgent.skills) ? rawAgent.skills : [];
|
|
189
|
+
const parsedSkills = rawSkills
|
|
190
|
+
.map((row) => {
|
|
191
|
+
if (!row || typeof row !== 'object') return null;
|
|
192
|
+
const skill = String(row.skill || '').trim();
|
|
193
|
+
const level = String(row.level || '').trim() || 'intermediate';
|
|
194
|
+
if (!skill) return null;
|
|
195
|
+
return { skill, level };
|
|
196
|
+
})
|
|
197
|
+
.filter(Boolean);
|
|
198
|
+
return {
|
|
199
|
+
id,
|
|
200
|
+
name: String(rawAgent.name || '').trim() || id,
|
|
201
|
+
role: roleType,
|
|
202
|
+
role_type: roleType,
|
|
203
|
+
knowledge_background: Array.isArray(rawAgent.knowledge_background)
|
|
204
|
+
? rawAgent.knowledge_background.map((row) => String(row || '').trim()).filter(Boolean)
|
|
205
|
+
: [],
|
|
206
|
+
skills: parsedSkills.length ? parsedSkills : (legacySkill ? [{ skill: legacySkill, level: 'intermediate' }] : []),
|
|
207
|
+
task_types: Array.isArray(rawAgent.task_types)
|
|
208
|
+
? rawAgent.task_types.map((row) => String(row || '').trim()).filter(Boolean)
|
|
209
|
+
: [],
|
|
210
|
+
deliverables: Array.isArray(rawAgent.deliverables)
|
|
211
|
+
? rawAgent.deliverables.map((row) => String(row || '').trim()).filter(Boolean)
|
|
212
|
+
: [],
|
|
213
|
+
raci_default: String(rawAgent.raci_default || '').trim(),
|
|
214
|
+
quality_criteria: Array.isArray(rawAgent.quality_criteria)
|
|
215
|
+
? rawAgent.quality_criteria.map((row) => String(row || '').trim()).filter(Boolean)
|
|
216
|
+
: [],
|
|
217
|
+
risk_limits: Array.isArray(rawAgent.risk_limits)
|
|
218
|
+
? rawAgent.risk_limits.map((row) => String(row || '').trim()).filter(Boolean)
|
|
219
|
+
: [],
|
|
220
|
+
created_at: String(rawAgent.created_at || new Date().toISOString()),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizeTeamRecord(rawTeam = {}) {
|
|
225
|
+
const id = String(rawTeam.id || '').trim() || `team-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
|
226
|
+
const agentIds = Array.isArray(rawTeam.agent_ids) ? rawTeam.agent_ids.map((idValue) => String(idValue || '').trim()).filter(Boolean) : [];
|
|
227
|
+
return {
|
|
228
|
+
id,
|
|
229
|
+
name: String(rawTeam.name || '').trim() || id,
|
|
230
|
+
agent_ids: Array.from(new Set(agentIds)),
|
|
231
|
+
created_at: String(rawTeam.created_at || new Date().toISOString()),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function loadAiEngineeringState() {
|
|
236
|
+
try {
|
|
237
|
+
const rawAgentClub = window.localStorage.getItem(AGENT_CLUB_STORAGE_KEY);
|
|
238
|
+
const rawTeams = window.localStorage.getItem(TEAMS_STORAGE_KEY);
|
|
239
|
+
const rawAssignments = window.localStorage.getItem(ASER_PROJECT_TEAM_ASSIGNMENTS_STORAGE_KEY);
|
|
240
|
+
const parsedAgentClub = safeParseJson(rawAgentClub, []);
|
|
241
|
+
const parsedTeams = safeParseJson(rawTeams, []);
|
|
242
|
+
const parsedAssignments = safeParseJson(rawAssignments, {});
|
|
243
|
+
state.agentClub = Array.isArray(parsedAgentClub) ? parsedAgentClub.map((row) => normalizeAgentRecord(row)) : [];
|
|
244
|
+
state.teams = Array.isArray(parsedTeams) ? parsedTeams.map((row) => normalizeTeamRecord(row)) : [];
|
|
245
|
+
state.aserProjectTeamAssignments = parsedAssignments && typeof parsedAssignments === 'object' ? parsedAssignments : {};
|
|
246
|
+
} catch {
|
|
247
|
+
state.agentClub = [];
|
|
248
|
+
state.teams = [];
|
|
249
|
+
state.aserProjectTeamAssignments = {};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function persistAiEngineeringState() {
|
|
254
|
+
try {
|
|
255
|
+
window.localStorage.setItem(AGENT_CLUB_STORAGE_KEY, JSON.stringify(state.agentClub));
|
|
256
|
+
window.localStorage.setItem(TEAMS_STORAGE_KEY, JSON.stringify(state.teams));
|
|
257
|
+
window.localStorage.setItem(
|
|
258
|
+
ASER_PROJECT_TEAM_ASSIGNMENTS_STORAGE_KEY,
|
|
259
|
+
JSON.stringify(state.aserProjectTeamAssignments || {}),
|
|
260
|
+
);
|
|
261
|
+
} catch {
|
|
262
|
+
// Ignore storage failures in restricted environments.
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function getAgentNameById(agentId) {
|
|
267
|
+
const id = String(agentId || '').trim();
|
|
268
|
+
const row = state.agentClub.find((agent) => String(agent.id) === id);
|
|
269
|
+
if (!row) return id || '-';
|
|
270
|
+
return `${row.name} (${row.role})`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function renderAgentClub() {
|
|
274
|
+
const list = document.getElementById('agent-club-list');
|
|
275
|
+
if (!(list instanceof HTMLElement)) return;
|
|
276
|
+
if (!state.agentClub.length) {
|
|
277
|
+
list.innerHTML = '<div class="aser-empty">暂无 Agent,可先创建。</div>';
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
list.innerHTML = state.agentClub
|
|
281
|
+
.map((agent) => {
|
|
282
|
+
const knowledgeText = Array.isArray(agent.knowledge_background) && agent.knowledge_background.length
|
|
283
|
+
? agent.knowledge_background.join(';')
|
|
284
|
+
: '-';
|
|
285
|
+
const skillsText = Array.isArray(agent.skills) && agent.skills.length
|
|
286
|
+
? agent.skills.map((row) => `${row.skill}(${row.level})`).join(';')
|
|
287
|
+
: '-';
|
|
288
|
+
const taskTypesText = Array.isArray(agent.task_types) && agent.task_types.length ? agent.task_types.join(';') : '-';
|
|
289
|
+
const deliverablesText = Array.isArray(agent.deliverables) && agent.deliverables.length ? agent.deliverables.join(';') : '-';
|
|
290
|
+
const qualityText = Array.isArray(agent.quality_criteria) && agent.quality_criteria.length ? agent.quality_criteria.join(';') : '-';
|
|
291
|
+
const riskText = Array.isArray(agent.risk_limits) && agent.risk_limits.length ? agent.risk_limits.join(';') : '-';
|
|
292
|
+
return `
|
|
293
|
+
<article class="aser-project-item">
|
|
294
|
+
<div>
|
|
295
|
+
<strong>${escapeHtml(agent.name)}</strong>
|
|
296
|
+
<div class="aser-project-id">${escapeHtml(agent.id)}</div>
|
|
297
|
+
<div class="aser-meta-item">角色:${escapeHtml(agent.role_type || agent.role || 'generalist')}</div>
|
|
298
|
+
<div class="aser-meta-item">知识背景:${escapeHtml(knowledgeText)}</div>
|
|
299
|
+
<div class="aser-meta-item">技能:${escapeHtml(skillsText)}</div>
|
|
300
|
+
<div class="aser-meta-item">任务类型:${escapeHtml(taskTypesText)}</div>
|
|
301
|
+
<div class="aser-meta-item">产物类型:${escapeHtml(deliverablesText)}</div>
|
|
302
|
+
<div class="aser-meta-item">RACI:${escapeHtml(agent.raci_default || '-')}</div>
|
|
303
|
+
<div class="aser-meta-item">质量标准:${escapeHtml(qualityText)}</div>
|
|
304
|
+
<div class="aser-meta-item">风险边界:${escapeHtml(riskText)}</div>
|
|
305
|
+
</div>
|
|
306
|
+
<button class="btn btn-secondary btn-sm" type="button" data-agent-delete-id="${escapeHtml(agent.id)}">删除</button>
|
|
307
|
+
</article>
|
|
308
|
+
`;
|
|
309
|
+
})
|
|
310
|
+
.join('');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderTeams() {
|
|
314
|
+
renderTeamAgentSelector();
|
|
315
|
+
const list = document.getElementById('teams-list');
|
|
316
|
+
if (!(list instanceof HTMLElement)) return;
|
|
317
|
+
if (!state.teams.length) {
|
|
318
|
+
list.innerHTML = '<div class="aser-empty">暂无 Team,可先创建。</div>';
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
list.innerHTML = state.teams
|
|
322
|
+
.map((team) => {
|
|
323
|
+
const memberText = team.agent_ids.length ? team.agent_ids.map((id) => escapeHtml(getAgentNameById(id))).join(',') : '无成员';
|
|
324
|
+
return `
|
|
325
|
+
<article class="aser-project-item">
|
|
326
|
+
<div>
|
|
327
|
+
<strong>${escapeHtml(team.name)}</strong>
|
|
328
|
+
<div class="aser-project-id">${escapeHtml(team.id)}</div>
|
|
329
|
+
<div class="aser-meta-item">成员:${memberText}</div>
|
|
330
|
+
</div>
|
|
331
|
+
<button class="btn btn-secondary btn-sm" type="button" data-team-delete-id="${escapeHtml(team.id)}">删除</button>
|
|
332
|
+
</article>
|
|
333
|
+
`;
|
|
334
|
+
})
|
|
335
|
+
.join('');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function renderTeamAgentSelector() {
|
|
339
|
+
const select = document.getElementById('teams-agent-select');
|
|
340
|
+
if (!(select instanceof HTMLSelectElement)) return;
|
|
341
|
+
if (!state.agentClub.length) {
|
|
342
|
+
select.innerHTML = '<option value="" disabled>暂无 Agent,请先到 Agent Club 创建</option>';
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
select.innerHTML = state.agentClub
|
|
346
|
+
.map((agent) => `<option value="${escapeHtml(agent.id)}">${escapeHtml(agent.name)} (${escapeHtml(agent.role)})</option>`)
|
|
347
|
+
.join('');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function assignAserProjectTeam(projectId, teamId) {
|
|
351
|
+
const pid = String(projectId || '').trim();
|
|
352
|
+
if (!pid) return;
|
|
353
|
+
const normalizedTeamId = String(teamId || '').trim();
|
|
354
|
+
if (normalizedTeamId) {
|
|
355
|
+
state.aserProjectTeamAssignments[pid] = normalizedTeamId;
|
|
356
|
+
} else {
|
|
357
|
+
delete state.aserProjectTeamAssignments[pid];
|
|
358
|
+
}
|
|
359
|
+
persistAiEngineeringState();
|
|
360
|
+
renderOpenCodeRunner();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function formatLocalTime(value) {
|
|
364
|
+
if (!value) return '-';
|
|
365
|
+
const date = new Date(value);
|
|
366
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
367
|
+
return date.toLocaleString();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function getTeamById(teamId) {
|
|
371
|
+
const id = String(teamId || '').trim();
|
|
372
|
+
if (!id) return null;
|
|
373
|
+
return state.teams.find((team) => String(team.id) === id) || null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getAssignedTeamIdForProject(projectId) {
|
|
377
|
+
const pid = String(projectId || '').trim();
|
|
378
|
+
if (!pid) return '';
|
|
379
|
+
return String(state.aserProjectTeamAssignments?.[pid] || '').trim();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function appendRunnerTimeline(row = {}) {
|
|
383
|
+
state.opencodeRunnerTimeline.unshift({
|
|
384
|
+
id: `runner-log-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
|
|
385
|
+
time: new Date().toISOString(),
|
|
386
|
+
...row,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function inferTaskTaxonomy(task, intents) {
|
|
391
|
+
const intentId = String(task?.intent_id || '').trim();
|
|
392
|
+
const intent = (Array.isArray(intents) ? intents : []).find((row) => String(row.id) === intentId);
|
|
393
|
+
return String(intent?.taxonomy || '').trim();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function inferCommunicationDemand(taxonomy) {
|
|
397
|
+
const value = String(taxonomy || '').trim();
|
|
398
|
+
if (value === 'clarify_design') return 'clarification';
|
|
399
|
+
if (value === 'escalate_issue' || value === 'report_issue') return 'escalation';
|
|
400
|
+
return '';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function getRoleArtifactTemplates(role) {
|
|
404
|
+
const value = String(role || '').trim();
|
|
405
|
+
if (value === 'product_manager') return [{ type: 'requirement_doc', summary: '需求分解与验收标准已提交' }];
|
|
406
|
+
if (value === 'architect') return [{ type: 'design_doc', summary: '系统设计方案与约束说明已提交' }];
|
|
407
|
+
if (value === 'reviewer') return [{ type: 'review_record', summary: '质量评审记录已提交' }];
|
|
408
|
+
return [
|
|
409
|
+
{ type: 'pr', summary: '实现代码与变更说明已提交' },
|
|
410
|
+
{ type: 'test_report', summary: '测试报告与通过结论已提交' },
|
|
411
|
+
];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function pickAgentForTask(team, task) {
|
|
415
|
+
const members = Array.isArray(team?.agent_ids) ? team.agent_ids : [];
|
|
416
|
+
const ownerRole = String(task?.owner_role || '').trim();
|
|
417
|
+
const exact = members
|
|
418
|
+
.map((id) => state.agentClub.find((agent) => String(agent.id) === String(id)))
|
|
419
|
+
.find((agent) => String(agent?.role_type || agent?.role || '').trim() === ownerRole);
|
|
420
|
+
if (exact) return exact;
|
|
421
|
+
return members
|
|
422
|
+
.map((id) => state.agentClub.find((agent) => String(agent.id) === String(id)))
|
|
423
|
+
.find(Boolean) || null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function loadOpenCodeRunnerProjects() {
|
|
427
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) return;
|
|
428
|
+
const projects = await apiRequest(`${API_BASE}/aser-runtime/projects`);
|
|
429
|
+
state.opencodeRunnerProjects = Array.isArray(projects) ? projects : [];
|
|
430
|
+
if (!state.opencodeRunnerSelectedProjectId && state.opencodeRunnerProjects.length) {
|
|
431
|
+
state.opencodeRunnerSelectedProjectId = String(state.opencodeRunnerProjects[0].id || '');
|
|
432
|
+
}
|
|
433
|
+
if (state.opencodeRunnerSelectedProjectId
|
|
434
|
+
&& !state.opencodeRunnerProjects.some((project) => String(project.id) === String(state.opencodeRunnerSelectedProjectId))) {
|
|
435
|
+
state.opencodeRunnerSelectedProjectId = state.opencodeRunnerProjects[0]
|
|
436
|
+
? String(state.opencodeRunnerProjects[0].id || '')
|
|
437
|
+
: '';
|
|
438
|
+
}
|
|
439
|
+
const assignedTeamId = getAssignedTeamIdForProject(state.opencodeRunnerSelectedProjectId);
|
|
440
|
+
state.opencodeRunnerSelectedTeamId = assignedTeamId || state.opencodeRunnerSelectedTeamId || String(state.teams[0]?.id || '');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function renderOpenCodeRunner() {
|
|
444
|
+
const projectSelect = document.getElementById('opencode-runner-project-select');
|
|
445
|
+
const teamSelect = document.getElementById('opencode-runner-team-select');
|
|
446
|
+
const taskList = document.getElementById('opencode-runner-task-list');
|
|
447
|
+
const timeline = document.getElementById('opencode-runner-timeline');
|
|
448
|
+
const summary = document.getElementById('opencode-runner-summary');
|
|
449
|
+
const loadBtn = document.getElementById('opencode-runner-load-btn');
|
|
450
|
+
const planBtn = document.getElementById('opencode-runner-plan-btn');
|
|
451
|
+
const startBtn = document.getElementById('opencode-runner-start-btn');
|
|
452
|
+
if (!(projectSelect instanceof HTMLSelectElement)
|
|
453
|
+
|| !(teamSelect instanceof HTMLSelectElement)
|
|
454
|
+
|| !(taskList instanceof HTMLElement)
|
|
455
|
+
|| !(timeline instanceof HTMLElement)
|
|
456
|
+
|| !(summary instanceof HTMLElement)) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (!state.opencodeRunnerProjects.length) {
|
|
461
|
+
projectSelect.innerHTML = '<option value="">暂无 ASER Project</option>';
|
|
462
|
+
} else {
|
|
463
|
+
projectSelect.innerHTML = state.opencodeRunnerProjects
|
|
464
|
+
.map((project) => `<option value="${escapeHtml(project.id)}">${escapeHtml(project.name || project.id)}</option>`)
|
|
465
|
+
.join('');
|
|
466
|
+
projectSelect.value = String(state.opencodeRunnerSelectedProjectId || state.opencodeRunnerProjects[0].id || '');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!state.teams.length) {
|
|
470
|
+
teamSelect.innerHTML = '<option value="">暂无 Team(先在 Teams 页面创建)</option>';
|
|
471
|
+
} else {
|
|
472
|
+
teamSelect.innerHTML = state.teams
|
|
473
|
+
.map((team) => `<option value="${escapeHtml(team.id)}">${escapeHtml(team.name || team.id)}</option>`)
|
|
474
|
+
.join('');
|
|
475
|
+
if (!state.opencodeRunnerSelectedTeamId || !state.teams.some((team) => String(team.id) === String(state.opencodeRunnerSelectedTeamId))) {
|
|
476
|
+
state.opencodeRunnerSelectedTeamId = String(state.teams[0].id || '');
|
|
477
|
+
}
|
|
478
|
+
teamSelect.value = state.opencodeRunnerSelectedTeamId;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const selectedTeam = getTeamById(state.opencodeRunnerSelectedTeamId);
|
|
482
|
+
const totalTasks = state.opencodeRunnerTasks.length;
|
|
483
|
+
const doneTasks = state.opencodeRunnerTasks.filter((row) => ['accepted', 'completed'].includes(String(row.status || ''))).length;
|
|
484
|
+
const commTasks = state.opencodeRunnerTasks.filter((row) => String(row.communication_demand || '')).length;
|
|
485
|
+
summary.textContent = `项目 ${state.opencodeRunnerSelectedProjectId || '-'} | Team ${selectedTeam?.name || '-'} | 任务 ${doneTasks}/${totalTasks} 已完成 | 沟通诉求 ${commTasks}`;
|
|
486
|
+
|
|
487
|
+
if (!state.opencodeRunnerTasks.length) {
|
|
488
|
+
taskList.innerHTML = '<div class="aser-empty">点击“获取任务”加载当前 Project 的任务池。</div>';
|
|
489
|
+
} else {
|
|
490
|
+
taskList.innerHTML = state.opencodeRunnerTasks
|
|
491
|
+
.map((task) => `
|
|
492
|
+
<article class="aser-project-item">
|
|
493
|
+
<div>
|
|
494
|
+
<strong>${escapeHtml(task.title || task.id)}</strong>
|
|
495
|
+
<div class="aser-project-id">${escapeHtml(task.id)}</div>
|
|
496
|
+
<div class="aser-meta-item">责任角色:${escapeHtml(task.owner_role || '-')} | Agent:${escapeHtml(task.assigned_agent_name || '-')}</div>
|
|
497
|
+
<div class="aser-meta-item">状态:${escapeHtml(task.status || '-')} | 意图:${escapeHtml(task.taxonomy || '-')}</div>
|
|
498
|
+
<div class="aser-meta-item">诉求:${escapeHtml(task.communication_demand || '-')}</div>
|
|
499
|
+
</div>
|
|
500
|
+
</article>
|
|
501
|
+
`)
|
|
502
|
+
.join('');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (!state.opencodeRunnerTimeline.length) {
|
|
506
|
+
timeline.innerHTML = '<div class="aser-empty">暂无过程日志。</div>';
|
|
507
|
+
} else {
|
|
508
|
+
timeline.innerHTML = state.opencodeRunnerTimeline
|
|
509
|
+
.map((row) => `
|
|
510
|
+
<article class="aser-chat-task-interaction">
|
|
511
|
+
<strong>${escapeHtml(row.phase || 'event')}</strong> · ${escapeHtml(formatLocalTime(row.time))}
|
|
512
|
+
<div>${escapeHtml(row.message || '-')}</div>
|
|
513
|
+
</article>
|
|
514
|
+
`)
|
|
515
|
+
.join('');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const writeDisabled = state.readonly || !state.fullAccess;
|
|
519
|
+
if (loadBtn instanceof HTMLButtonElement) loadBtn.disabled = state.opencodeRunnerRunning;
|
|
520
|
+
if (planBtn instanceof HTMLButtonElement) planBtn.disabled = state.opencodeRunnerRunning || !state.opencodeRunnerTasks.length;
|
|
521
|
+
if (startBtn instanceof HTMLButtonElement) {
|
|
522
|
+
startBtn.disabled = writeDisabled
|
|
523
|
+
|| state.opencodeRunnerRunning
|
|
524
|
+
|| !state.opencodeRunnerTasks.length
|
|
525
|
+
|| !state.opencodeRunnerSelectedTeamId;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function loadOpenCodeRunnerTasks() {
|
|
530
|
+
const projectId = String(state.opencodeRunnerSelectedProjectId || '').trim();
|
|
531
|
+
if (!projectId) return;
|
|
532
|
+
const detail = await apiRequest(`${API_BASE}/aser-runtime/projects/${projectId}`);
|
|
533
|
+
const intents = Array.isArray(detail?.intents) ? detail.intents : [];
|
|
534
|
+
const selectedTeam = getTeamById(state.opencodeRunnerSelectedTeamId);
|
|
535
|
+
const tasks = Array.isArray(detail?.tasks) ? detail.tasks : [];
|
|
536
|
+
state.opencodeRunnerTasks = tasks.map((task) => {
|
|
537
|
+
const taxonomy = inferTaskTaxonomy(task, intents);
|
|
538
|
+
const communicationDemand = inferCommunicationDemand(taxonomy);
|
|
539
|
+
const assigned = pickAgentForTask(selectedTeam, task);
|
|
540
|
+
return {
|
|
541
|
+
id: String(task.id || ''),
|
|
542
|
+
title: String(task.title || ''),
|
|
543
|
+
owner_role: String(task.owner_role || ''),
|
|
544
|
+
status: String(task.status || ''),
|
|
545
|
+
taxonomy,
|
|
546
|
+
communication_demand: communicationDemand,
|
|
547
|
+
assigned_agent_id: String(assigned?.id || ''),
|
|
548
|
+
assigned_agent_name: String(assigned?.name || ''),
|
|
549
|
+
};
|
|
550
|
+
});
|
|
551
|
+
state.opencodeRunnerPlanned = false;
|
|
552
|
+
appendRunnerTimeline({
|
|
553
|
+
phase: 'tasks_fetched',
|
|
554
|
+
message: `已获取任务 ${state.opencodeRunnerTasks.length} 条,准备进入编排。`,
|
|
555
|
+
});
|
|
556
|
+
renderOpenCodeRunner();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function planOpenCodeRunnerTasks() {
|
|
560
|
+
if (!state.opencodeRunnerTasks.length) return;
|
|
561
|
+
state.opencodeRunnerTasks.forEach((task) => {
|
|
562
|
+
appendRunnerTimeline({
|
|
563
|
+
phase: 'dispatch',
|
|
564
|
+
message: `任务 ${task.title || task.id} -> ${task.owner_role || '-'} / ${task.assigned_agent_name || '-'}`,
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
state.opencodeRunnerPlanned = true;
|
|
568
|
+
renderOpenCodeRunner();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function startOpenCodeRunner() {
|
|
572
|
+
const projectId = String(state.opencodeRunnerSelectedProjectId || '').trim();
|
|
573
|
+
const teamId = String(state.opencodeRunnerSelectedTeamId || '').trim();
|
|
574
|
+
if (!projectId || !teamId || state.opencodeRunnerRunning) return;
|
|
575
|
+
const team = getTeamById(teamId);
|
|
576
|
+
if (!team) return;
|
|
577
|
+
|
|
578
|
+
state.opencodeRunnerRunning = true;
|
|
579
|
+
renderOpenCodeRunner();
|
|
580
|
+
appendRunnerTimeline({ phase: 'run_start', message: `启动 OpenCode Team Runner,Team=${team.name}` });
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
for (const task of state.opencodeRunnerTasks) {
|
|
584
|
+
const status = String(task.status || '');
|
|
585
|
+
if (status === 'accepted' || status === 'completed') {
|
|
586
|
+
appendRunnerTimeline({ phase: 'skip', message: `跳过已完成任务:${task.title || task.id}` });
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const role = String(task.owner_role || 'developer') || 'developer';
|
|
590
|
+
appendRunnerTimeline({ phase: 'agent_start', message: `启动 Agent:${task.assigned_agent_name || role},处理任务 ${task.title || task.id}` });
|
|
591
|
+
|
|
592
|
+
const pulled = await apiRequest(`${API_BASE}/aser-runtime/projects/${projectId}/pull-task?role=${encodeURIComponent(role)}`);
|
|
593
|
+
const pulledTaskId = String(pulled?.task?.id || '');
|
|
594
|
+
if (!pulledTaskId) {
|
|
595
|
+
appendRunnerTimeline({ phase: 'blocked', message: `角色 ${role} 当前无可拉取任务,等待调度。` });
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const artifacts = getRoleArtifactTemplates(role).map((row, index) => ({
|
|
600
|
+
type: row.type,
|
|
601
|
+
uri: `opencode://runner/${projectId}/${pulledTaskId}/${index + 1}`,
|
|
602
|
+
summary: row.summary,
|
|
603
|
+
}));
|
|
604
|
+
await apiRequest(`${API_BASE}/aser-runtime/projects/${projectId}/tasks/${pulledTaskId}/submit-result`, {
|
|
605
|
+
method: 'POST',
|
|
606
|
+
body: JSON.stringify({
|
|
607
|
+
role,
|
|
608
|
+
summary: `OpenCode Runner completed by ${task.assigned_agent_name || role}`,
|
|
609
|
+
artifacts,
|
|
610
|
+
}),
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const decision = await apiRequest(`${API_BASE}/aser-runtime/projects/${projectId}/tasks/${pulledTaskId}/evaluations`, {
|
|
614
|
+
method: 'POST',
|
|
615
|
+
body: JSON.stringify({
|
|
616
|
+
evaluator: task.assigned_agent_name || `Runner-${role}`,
|
|
617
|
+
decision: 'accept',
|
|
618
|
+
score: 90,
|
|
619
|
+
comments: 'OpenCode Team Runner: auto-accepted by orchestration policy',
|
|
620
|
+
}),
|
|
621
|
+
});
|
|
622
|
+
appendRunnerTimeline({
|
|
623
|
+
phase: 'agent_done',
|
|
624
|
+
message: `任务 ${task.title || pulledTaskId} 完成,评价=${String(decision?.decision || 'accept')}`,
|
|
625
|
+
});
|
|
626
|
+
if (task.communication_demand === 'clarification') {
|
|
627
|
+
appendRunnerTimeline({ phase: 'communication', message: `澄清诉求:${task.title || pulledTaskId} 需要进一步业务澄清。` });
|
|
628
|
+
}
|
|
629
|
+
if (task.communication_demand === 'escalation') {
|
|
630
|
+
appendRunnerTimeline({ phase: 'communication', message: `上升诉求:${task.title || pulledTaskId} 存在风险需上升管理。` });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
appendRunnerTimeline({ phase: 'run_done', message: 'OpenCode Team Runner 执行结束。' });
|
|
634
|
+
await loadOpenCodeRunnerTasks();
|
|
635
|
+
} catch (error) {
|
|
636
|
+
appendRunnerTimeline({ phase: 'run_error', message: `执行失败:${error?.message || error}` });
|
|
637
|
+
} finally {
|
|
638
|
+
state.opencodeRunnerRunning = false;
|
|
639
|
+
renderOpenCodeRunner();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function compareSandboxesBySortMode(a, b) {
|
|
644
|
+
const mode = String(state.sandboxSortMode || 'created_asc');
|
|
645
|
+
const createdA = String(a?.created_at || '');
|
|
646
|
+
const createdB = String(b?.created_at || '');
|
|
647
|
+
const updatedA = String(a?.updated_at || '');
|
|
648
|
+
const updatedB = String(b?.updated_at || '');
|
|
649
|
+
const nameA = String(a?.name || '');
|
|
650
|
+
const nameB = String(b?.name || '');
|
|
651
|
+
if (mode === 'created_desc') {
|
|
652
|
+
if (createdA !== createdB) return createdB.localeCompare(createdA);
|
|
653
|
+
} else if (mode === 'updated_desc') {
|
|
654
|
+
if (updatedA !== updatedB) return updatedB.localeCompare(updatedA);
|
|
655
|
+
} else if (mode === 'name_asc') {
|
|
656
|
+
if (nameA !== nameB) return nameA.localeCompare(nameB, 'zh-Hans-CN');
|
|
657
|
+
} else {
|
|
658
|
+
if (createdA !== createdB) return createdA.localeCompare(createdB);
|
|
659
|
+
}
|
|
660
|
+
return String(a?.id || '').localeCompare(String(b?.id || ''));
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function applySandboxSortControl() {
|
|
664
|
+
const select = document.getElementById('sandbox-sort-mode');
|
|
665
|
+
if (!(select instanceof HTMLSelectElement)) return;
|
|
666
|
+
const mode = String(state.sandboxSortMode || 'created_asc');
|
|
667
|
+
if (select.value !== mode) {
|
|
668
|
+
select.value = mode;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
99
672
|
function applySandboxChatVisibility() {
|
|
100
673
|
const layout = document.getElementById('sandbox-layout');
|
|
101
674
|
const toggleBtn = document.getElementById('toggle-sandbox-chat-btn');
|
|
@@ -171,10 +744,34 @@ function applyReadonlyMode() {
|
|
|
171
744
|
if (diariesNav instanceof HTMLElement) {
|
|
172
745
|
diariesNav.classList.toggle('hidden', !state.pageAccess.diaries && !state.fullAccess);
|
|
173
746
|
}
|
|
747
|
+
const tasksNav = document.querySelector('[data-nav="tasks"]');
|
|
748
|
+
if (tasksNav instanceof HTMLElement) {
|
|
749
|
+
tasksNav.classList.toggle('hidden', !state.pageAccess.sandboxes && !state.fullAccess);
|
|
750
|
+
}
|
|
174
751
|
const changesNav = document.querySelector('[data-nav="changes"]');
|
|
175
752
|
if (changesNav instanceof HTMLElement) {
|
|
176
753
|
changesNav.classList.toggle('hidden', !state.pageAccess.changes && !state.fullAccess);
|
|
177
754
|
}
|
|
755
|
+
const aserRuntimeNav = document.querySelector('[data-nav="aser-runtime"]');
|
|
756
|
+
if (aserRuntimeNav instanceof HTMLElement) {
|
|
757
|
+
aserRuntimeNav.classList.toggle('hidden', !state.pageAccess.sandboxes && !state.fullAccess);
|
|
758
|
+
}
|
|
759
|
+
const aiEngineeringNav = document.querySelector('[data-nav="ai-engineering"]');
|
|
760
|
+
if (aiEngineeringNav instanceof HTMLElement) {
|
|
761
|
+
aiEngineeringNav.classList.toggle('hidden', !state.pageAccess.sandboxes && !state.fullAccess);
|
|
762
|
+
}
|
|
763
|
+
const agentClubNav = document.querySelector('[data-nav="agent-club"]');
|
|
764
|
+
if (agentClubNav instanceof HTMLElement) {
|
|
765
|
+
agentClubNav.classList.toggle('hidden', !state.pageAccess.sandboxes && !state.fullAccess);
|
|
766
|
+
}
|
|
767
|
+
const teamsNav = document.querySelector('[data-nav="teams"]');
|
|
768
|
+
if (teamsNav instanceof HTMLElement) {
|
|
769
|
+
teamsNav.classList.toggle('hidden', !state.pageAccess.sandboxes && !state.fullAccess);
|
|
770
|
+
}
|
|
771
|
+
const opencodeRunnerNav = document.querySelector('[data-nav="opencode-team-runner"]');
|
|
772
|
+
if (opencodeRunnerNav instanceof HTMLElement) {
|
|
773
|
+
opencodeRunnerNav.classList.toggle('hidden', !state.pageAccess.sandboxes && !state.fullAccess);
|
|
774
|
+
}
|
|
178
775
|
const projectSettingsNav = document.querySelector('[data-nav="settings"]');
|
|
179
776
|
if (projectSettingsNav instanceof HTMLElement) {
|
|
180
777
|
projectSettingsNav.classList.toggle('hidden', !state.pageAccess.settings);
|
|
@@ -184,6 +781,7 @@ function applyReadonlyMode() {
|
|
|
184
781
|
systemSettingsNav.classList.toggle('hidden', !state.canAccessSystemSettings);
|
|
185
782
|
}
|
|
186
783
|
setHiddenById('add-sandbox-btn', state.readonly || !state.fullAccess);
|
|
784
|
+
setHiddenById('import-sandbox-btn', state.readonly || !state.fullAccess);
|
|
187
785
|
setHiddenById('add-item-btn', state.readonly || !state.currentSandboxWritable);
|
|
188
786
|
setHiddenById('toggle-node-entity-form-btn', state.readonly || !state.currentSandboxWritable);
|
|
189
787
|
setHiddenById('drawer-diary-save-btn', state.readonly || !state.currentSandboxWritable);
|
|
@@ -201,6 +799,12 @@ function applyReadonlyMode() {
|
|
|
201
799
|
closeQuickChatPopover();
|
|
202
800
|
}
|
|
203
801
|
applySandboxChatVisibility();
|
|
802
|
+
if (aserRuntimeView) {
|
|
803
|
+
aserRuntimeView.setAccessContext({
|
|
804
|
+
readonly: state.readonly,
|
|
805
|
+
hasAccess: state.fullAccess || state.pageAccess.sandboxes,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
204
808
|
}
|
|
205
809
|
|
|
206
810
|
function getSandboxLayoutElement() {
|
|
@@ -776,6 +1380,25 @@ function populateParentSelect(items, preferredParentId = null) {
|
|
|
776
1380
|
select.value = hasExpectedValue ? expectedValue : '';
|
|
777
1381
|
}
|
|
778
1382
|
|
|
1383
|
+
function getTodoMetaFromItem(item) {
|
|
1384
|
+
const extraData = item?.extra_data || {};
|
|
1385
|
+
const todo = extraData?.todo && typeof extraData.todo === 'object' ? extraData.todo : {};
|
|
1386
|
+
const plannedStartDate = String(todo.planned_start_date || '').trim();
|
|
1387
|
+
const dueDate = String(todo.due_date || '').trim();
|
|
1388
|
+
return { plannedStartDate, dueDate };
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function isTodoItem(item) {
|
|
1392
|
+
return Boolean(item?.extra_data?.todo?.is_todo);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function getNodeTodoItems(nodeId) {
|
|
1396
|
+
const items = state.currentSandbox?.items || [];
|
|
1397
|
+
return items
|
|
1398
|
+
.filter((item) => String(item.parent_id || '') === String(nodeId || '') && isTodoItem(item))
|
|
1399
|
+
.sort((a, b) => String(a.created_at || '').localeCompare(String(b.created_at || '')));
|
|
1400
|
+
}
|
|
1401
|
+
|
|
779
1402
|
function normalizeAssistantResponseText(raw) {
|
|
780
1403
|
let text = String(raw || '').trim();
|
|
781
1404
|
if (!text) return '';
|
|
@@ -1150,12 +1773,14 @@ function renderNodeEntitySummary(nodeId) {
|
|
|
1150
1773
|
const issues = rows.filter((row) => row.entity_type === 'issue');
|
|
1151
1774
|
const knowledges = rows.filter((row) => row.entity_type === 'knowledge');
|
|
1152
1775
|
const capabilities = rows.filter((row) => row.entity_type === 'capability');
|
|
1776
|
+
const todos = getNodeTodoItems(nodeId);
|
|
1153
1777
|
const openIssues = issues.filter((row) => row.status === 'open' || row.status === 'in_progress' || row.status === 'blocked').length;
|
|
1154
1778
|
container.innerHTML = `
|
|
1155
1779
|
<div class="summary-card"><div class="label">Issue</div><div class="value">${issues.length}</div></div>
|
|
1156
1780
|
<div class="summary-card"><div class="label">Open Issue</div><div class="value">${openIssues}</div></div>
|
|
1157
1781
|
<div class="summary-card"><div class="label">Knowledge</div><div class="value">${knowledges.length}</div></div>
|
|
1158
1782
|
<div class="summary-card"><div class="label">Capability</div><div class="value">${capabilities.length}</div></div>
|
|
1783
|
+
<div class="summary-card"><div class="label">Todo</div><div class="value">${todos.length}</div></div>
|
|
1159
1784
|
<div class="summary-card"><div class="label">Diary</div><div class="value">${diaries.length}</div></div>
|
|
1160
1785
|
`;
|
|
1161
1786
|
}
|
|
@@ -1180,8 +1805,10 @@ function renderNodeEntityList(nodeId) {
|
|
|
1180
1805
|
if (!container) return;
|
|
1181
1806
|
const allEntityRows = getNodeEntitiesByNodeId(nodeId);
|
|
1182
1807
|
const allDiaryRows = getNodeDiariesByNodeId(nodeId);
|
|
1808
|
+
const allTodoRows = getNodeTodoItems(nodeId);
|
|
1183
1809
|
const timelineRows = [
|
|
1184
1810
|
...allEntityRows.map((row) => ({ ...row, timeline_type: row.entity_type || 'issue' })),
|
|
1811
|
+
...allTodoRows.map((row) => ({ ...row, timeline_type: 'todo' })),
|
|
1185
1812
|
...allDiaryRows.map((row) => ({ ...row, timeline_type: 'diary' })),
|
|
1186
1813
|
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
1187
1814
|
const rows = state.nodeEntityFilter === 'all'
|
|
@@ -1218,6 +1845,32 @@ function renderNodeEntityList(nodeId) {
|
|
|
1218
1845
|
</div>
|
|
1219
1846
|
`;
|
|
1220
1847
|
}
|
|
1848
|
+
if (row.timeline_type === 'todo') {
|
|
1849
|
+
const todoMeta = getTodoMetaFromItem(row);
|
|
1850
|
+
return `
|
|
1851
|
+
<div class="entity-card">
|
|
1852
|
+
<div class="entity-card-header">
|
|
1853
|
+
<div>
|
|
1854
|
+
<span class="entity-type-pill">todo</span>
|
|
1855
|
+
<strong>${safeText(row.name || '-')}</strong>
|
|
1856
|
+
</div>
|
|
1857
|
+
${state.readonly ? '' : `
|
|
1858
|
+
<div class="entity-card-actions">
|
|
1859
|
+
<button class="btn btn-secondary btn-sm" data-todo-edit-id="${safeText(row.id)}">编辑</button>
|
|
1860
|
+
<button class="btn btn-secondary btn-sm" data-todo-delete-id="${safeText(row.id)}">删除</button>
|
|
1861
|
+
</div>
|
|
1862
|
+
`}
|
|
1863
|
+
</div>
|
|
1864
|
+
<div class="entity-meta">
|
|
1865
|
+
${safeText(new Date(row.created_at).toLocaleString())}
|
|
1866
|
+
${row.status ? ` · <span class="entity-status-pill ${safeText(row.status)}">${safeText(row.status)}</span>` : ''}
|
|
1867
|
+
${row.assignee ? ` · @${safeText(row.assignee)}` : ''}
|
|
1868
|
+
${todoMeta.plannedStartDate ? ` · 启动 ${safeText(todoMeta.plannedStartDate)}` : ''}
|
|
1869
|
+
${todoMeta.dueDate ? ` · 截止 ${safeText(todoMeta.dueDate)}` : ''}
|
|
1870
|
+
</div>
|
|
1871
|
+
</div>
|
|
1872
|
+
`;
|
|
1873
|
+
}
|
|
1221
1874
|
return `
|
|
1222
1875
|
<div class="entity-card">
|
|
1223
1876
|
<div class="entity-card-header">
|
|
@@ -1237,6 +1890,7 @@ function renderNodeEntityList(nodeId) {
|
|
|
1237
1890
|
${row.status ? ` · <span class="entity-status-pill ${safeText(row.status)}">${safeText(row.status)}</span>` : ''}
|
|
1238
1891
|
${row.priority ? ` · ${safeText(row.priority)}` : ''}
|
|
1239
1892
|
${row.assignee ? ` · @${safeText(row.assignee)}` : ''}
|
|
1893
|
+
${row.due_date ? ` · 截止 ${safeText(row.due_date)}` : ''}
|
|
1240
1894
|
</div>
|
|
1241
1895
|
<div class="entity-content">${renderMarkdownSnippet(row.content_md || '')}</div>
|
|
1242
1896
|
</div>
|
|
@@ -1264,6 +1918,41 @@ function renderNodeEntityList(nodeId) {
|
|
|
1264
1918
|
});
|
|
1265
1919
|
});
|
|
1266
1920
|
|
|
1921
|
+
container.querySelectorAll('[data-todo-edit-id]').forEach((el) => {
|
|
1922
|
+
el.addEventListener('click', (e) => {
|
|
1923
|
+
e.preventDefault();
|
|
1924
|
+
const id = el.getAttribute('data-todo-edit-id');
|
|
1925
|
+
if (!id) return;
|
|
1926
|
+
const row = allTodoRows.find((item) => item.id === id);
|
|
1927
|
+
if (!row) return;
|
|
1928
|
+
startEditNodeEntity({
|
|
1929
|
+
id: row.id,
|
|
1930
|
+
entity_type: 'todo',
|
|
1931
|
+
title: row.name,
|
|
1932
|
+
content_md: row.description || '',
|
|
1933
|
+
assignee: row.assignee || '',
|
|
1934
|
+
status: row.status || 'pending',
|
|
1935
|
+
priority: row.priority || 'medium',
|
|
1936
|
+
capability_type: '',
|
|
1937
|
+
due_date: '',
|
|
1938
|
+
todo_planned_start_date: getTodoMetaFromItem(row).plannedStartDate,
|
|
1939
|
+
todo_due_date: getTodoMetaFromItem(row).dueDate,
|
|
1940
|
+
});
|
|
1941
|
+
});
|
|
1942
|
+
});
|
|
1943
|
+
container.querySelectorAll('[data-todo-delete-id]').forEach((el) => {
|
|
1944
|
+
el.addEventListener('click', async (e) => {
|
|
1945
|
+
e.preventDefault();
|
|
1946
|
+
if (!state.currentSandbox) return;
|
|
1947
|
+
const id = String(el.getAttribute('data-todo-delete-id') || '');
|
|
1948
|
+
if (!id) return;
|
|
1949
|
+
if (!confirm('确定删除该待办?')) return;
|
|
1950
|
+
await apiRequest(`${API_BASE}/items/${id}`, { method: 'DELETE' });
|
|
1951
|
+
await loadSandbox(state.currentSandbox.id);
|
|
1952
|
+
if (state.selectedNodeId) showNodeEntityDrawer(state.selectedNodeId, state.nodeEntityFilter);
|
|
1953
|
+
});
|
|
1954
|
+
});
|
|
1955
|
+
|
|
1267
1956
|
container.querySelectorAll('[data-diary-edit-id]').forEach((el) => {
|
|
1268
1957
|
el.addEventListener('click', (e) => {
|
|
1269
1958
|
e.preventDefault();
|
|
@@ -1300,6 +1989,7 @@ function resetNodeEntityForm() {
|
|
|
1300
1989
|
const titleInput = document.getElementById('entity-title-input');
|
|
1301
1990
|
const contentInput = document.getElementById('entity-content-input');
|
|
1302
1991
|
const assigneeInput = document.getElementById('entity-assignee-input');
|
|
1992
|
+
const dueDateInput = document.getElementById('entity-due-date-input');
|
|
1303
1993
|
const statusInput = document.getElementById('entity-status-select');
|
|
1304
1994
|
const priorityInput = document.getElementById('entity-priority-select');
|
|
1305
1995
|
const capabilityTypeInput = document.getElementById('entity-capability-type-input');
|
|
@@ -1307,6 +1997,11 @@ function resetNodeEntityForm() {
|
|
|
1307
1997
|
if (titleInput) titleInput.value = '';
|
|
1308
1998
|
if (contentInput) contentInput.value = '';
|
|
1309
1999
|
if (assigneeInput) assigneeInput.value = '';
|
|
2000
|
+
if (dueDateInput) dueDateInput.value = '';
|
|
2001
|
+
const todoPlannedStartInput = document.getElementById('entity-todo-planned-start-input');
|
|
2002
|
+
const todoDueDateInput = document.getElementById('entity-todo-due-date-input');
|
|
2003
|
+
if (todoPlannedStartInput) todoPlannedStartInput.value = '';
|
|
2004
|
+
if (todoDueDateInput) todoDueDateInput.value = '';
|
|
1310
2005
|
if (statusInput) statusInput.value = '';
|
|
1311
2006
|
if (priorityInput) priorityInput.value = '';
|
|
1312
2007
|
if (capabilityTypeInput) capabilityTypeInput.value = '';
|
|
@@ -1333,20 +2028,27 @@ function ensureCapabilityTypeOption(value) {
|
|
|
1333
2028
|
}
|
|
1334
2029
|
|
|
1335
2030
|
function startEditNodeEntity(row) {
|
|
1336
|
-
|
|
2031
|
+
const rowType = String(row.entity_type || 'issue');
|
|
2032
|
+
state.editingNodeEntityId = rowType === 'todo' ? `todo:${row.id}` : row.id;
|
|
1337
2033
|
setNodeEntityFormExpanded(true);
|
|
1338
2034
|
const typeInput = document.getElementById('entity-type-select');
|
|
1339
2035
|
const titleInput = document.getElementById('entity-title-input');
|
|
1340
2036
|
const contentInput = document.getElementById('entity-content-input');
|
|
1341
2037
|
const assigneeInput = document.getElementById('entity-assignee-input');
|
|
2038
|
+
const dueDateInput = document.getElementById('entity-due-date-input');
|
|
1342
2039
|
const statusInput = document.getElementById('entity-status-select');
|
|
1343
2040
|
const priorityInput = document.getElementById('entity-priority-select');
|
|
1344
2041
|
const capabilityTypeInput = document.getElementById('entity-capability-type-input');
|
|
1345
|
-
if (typeInput) typeInput.value =
|
|
2042
|
+
if (typeInput) typeInput.value = rowType || 'todo';
|
|
1346
2043
|
applyEntityFormMode();
|
|
1347
|
-
if (titleInput) titleInput.value = row.title || '';
|
|
2044
|
+
if (titleInput) titleInput.value = rowType === 'todo' ? (row.title || row.name || '') : (row.title || '');
|
|
1348
2045
|
if (contentInput) contentInput.value = row.content_md || '';
|
|
1349
2046
|
if (assigneeInput) assigneeInput.value = row.assignee || '';
|
|
2047
|
+
if (dueDateInput) dueDateInput.value = row.due_date || '';
|
|
2048
|
+
const todoPlannedStartInput = document.getElementById('entity-todo-planned-start-input');
|
|
2049
|
+
const todoDueDateInput = document.getElementById('entity-todo-due-date-input');
|
|
2050
|
+
if (todoPlannedStartInput) todoPlannedStartInput.value = row.todo_planned_start_date || '';
|
|
2051
|
+
if (todoDueDateInput) todoDueDateInput.value = row.todo_due_date || '';
|
|
1350
2052
|
if (statusInput) statusInput.value = row.status || '';
|
|
1351
2053
|
if (priorityInput) priorityInput.value = row.priority || '';
|
|
1352
2054
|
ensureCapabilityTypeOption(row.capability_type || '');
|
|
@@ -1365,7 +2067,7 @@ function showNodeEntityDrawer(nodeId, preferredFilter = 'all') {
|
|
|
1365
2067
|
const node = getNodeById(nodeId);
|
|
1366
2068
|
if (!drawer || !title || !node) return;
|
|
1367
2069
|
state.selectedNodeId = nodeId;
|
|
1368
|
-
const filter = ['all', 'issue', 'knowledge', 'capability', 'diary'].includes(preferredFilter) ? preferredFilter : 'all';
|
|
2070
|
+
const filter = ['all', 'todo', 'issue', 'knowledge', 'capability', 'diary'].includes(preferredFilter) ? preferredFilter : 'all';
|
|
1369
2071
|
state.nodeEntityFilter = filter;
|
|
1370
2072
|
title.textContent = node.name || nodeId;
|
|
1371
2073
|
renderNodeEntitySummary(nodeId);
|
|
@@ -1388,6 +2090,14 @@ function renderNodeEntityFilterTabs() {
|
|
|
1388
2090
|
}
|
|
1389
2091
|
|
|
1390
2092
|
function getStatusOptionsByEntityType(entityType) {
|
|
2093
|
+
if (entityType === 'todo') {
|
|
2094
|
+
return [
|
|
2095
|
+
{ value: 'pending', label: 'pending' },
|
|
2096
|
+
{ value: 'in_progress', label: 'in_progress' },
|
|
2097
|
+
{ value: 'done', label: 'done' },
|
|
2098
|
+
{ value: 'archived', label: 'archived' },
|
|
2099
|
+
];
|
|
2100
|
+
}
|
|
1391
2101
|
if (entityType === 'issue') {
|
|
1392
2102
|
return [
|
|
1393
2103
|
{ value: '', label: '状态(Issue,默认 open)' },
|
|
@@ -1411,11 +2121,14 @@ function getStatusOptionsByEntityType(entityType) {
|
|
|
1411
2121
|
function applyEntityFormMode() {
|
|
1412
2122
|
const type = document.getElementById('entity-type-select')?.value || 'issue';
|
|
1413
2123
|
const assigneeGroup = document.getElementById('entity-assignee-group');
|
|
2124
|
+
const dueDateGroup = document.getElementById('entity-due-date-group');
|
|
2125
|
+
const todoDateGroup = document.getElementById('entity-todo-date-group');
|
|
1414
2126
|
const statusGroup = document.getElementById('entity-status-group');
|
|
1415
2127
|
const priorityGroup = document.getElementById('entity-priority-group');
|
|
1416
2128
|
const capabilityTypeGroup = document.getElementById('entity-capability-type-group');
|
|
1417
2129
|
const hint = document.getElementById('entity-form-hint');
|
|
1418
2130
|
const statusSelect = document.getElementById('entity-status-select');
|
|
2131
|
+
const contentInput = document.getElementById('entity-content-input');
|
|
1419
2132
|
|
|
1420
2133
|
if (statusSelect) {
|
|
1421
2134
|
statusSelect.innerHTML = getStatusOptionsByEntityType(type)
|
|
@@ -1423,17 +2136,22 @@ function applyEntityFormMode() {
|
|
|
1423
2136
|
.join('');
|
|
1424
2137
|
}
|
|
1425
2138
|
|
|
1426
|
-
assigneeGroup?.classList.toggle('hidden', type
|
|
1427
|
-
|
|
2139
|
+
assigneeGroup?.classList.toggle('hidden', !(type === 'issue' || type === 'todo'));
|
|
2140
|
+
dueDateGroup?.classList.toggle('hidden', type !== 'issue');
|
|
2141
|
+
todoDateGroup?.classList.toggle('hidden', type !== 'todo');
|
|
2142
|
+
priorityGroup?.classList.toggle('hidden', !(type === 'issue' || type === 'todo'));
|
|
1428
2143
|
capabilityTypeGroup?.classList.toggle('hidden', type !== 'capability');
|
|
1429
2144
|
statusGroup?.classList.toggle('hidden', false);
|
|
2145
|
+
contentInput?.classList.toggle('hidden', type === 'todo');
|
|
1430
2146
|
|
|
1431
2147
|
if (hint) {
|
|
1432
|
-
hint.textContent = type === '
|
|
1433
|
-
? '
|
|
1434
|
-
: type === '
|
|
1435
|
-
? '
|
|
1436
|
-
:
|
|
2148
|
+
hint.textContent = type === 'todo'
|
|
2149
|
+
? 'Todo 将作为当前节点子任务创建。'
|
|
2150
|
+
: type === 'issue'
|
|
2151
|
+
? 'Issue 推荐填写状态/优先级/负责人。'
|
|
2152
|
+
: type === 'knowledge'
|
|
2153
|
+
? 'Knowledge 可仅保存链接或少量 Markdown。'
|
|
2154
|
+
: 'Capability 建议填写能力类型与简述。';
|
|
1437
2155
|
}
|
|
1438
2156
|
}
|
|
1439
2157
|
|
|
@@ -1672,23 +2390,128 @@ function renderSandboxesSummary() {
|
|
|
1672
2390
|
function renderSandboxes() {
|
|
1673
2391
|
const grid = document.getElementById('sandbox-grid');
|
|
1674
2392
|
if (!grid) return;
|
|
1675
|
-
|
|
2393
|
+
const sortedSandboxes = [...state.sandboxes].sort(compareSandboxesBySortMode);
|
|
2394
|
+
applySandboxSortControl();
|
|
1676
2395
|
mountSandboxGrid('sandbox-grid', {
|
|
1677
|
-
sandboxes:
|
|
2396
|
+
sandboxes: sortedSandboxes,
|
|
1678
2397
|
emptyText: '暂无沙盘',
|
|
1679
2398
|
onOpen: (id) => {
|
|
1680
2399
|
window.location.hash = `/sandbox/${id}`;
|
|
1681
2400
|
},
|
|
2401
|
+
onEdit: (state.readonly || !state.fullAccess) ? undefined : (id) => {
|
|
2402
|
+
const current = state.sandboxes.find((sandbox) => sandbox.id === id);
|
|
2403
|
+
if (!current) return;
|
|
2404
|
+
const dialog = document.getElementById('sandbox-dialog');
|
|
2405
|
+
const title = document.getElementById('sandbox-dialog-title');
|
|
2406
|
+
const confirmBtn = document.getElementById('confirm-sandbox-btn');
|
|
2407
|
+
if (!dialog || !title || !confirmBtn) return;
|
|
2408
|
+
title.textContent = '编辑沙盘';
|
|
2409
|
+
confirmBtn.textContent = '保存';
|
|
2410
|
+
dialog.dataset.editId = String(id);
|
|
2411
|
+
document.getElementById('new-sandbox-name').value = String(current.name || '');
|
|
2412
|
+
document.getElementById('new-sandbox-desc').value = String(current.description || '');
|
|
2413
|
+
dialog.showModal();
|
|
2414
|
+
},
|
|
2415
|
+
onCopyLink: (state.readonly || !state.fullAccess) ? undefined : async (id) => {
|
|
2416
|
+
try {
|
|
2417
|
+
const data = await apiRequest(`${API_BASE}/sandboxes/${id}/copy-link`, { method: 'POST' });
|
|
2418
|
+
const copyLink = String(data?.copy_link || '');
|
|
2419
|
+
if (!copyLink) throw new Error('复制链接为空');
|
|
2420
|
+
await navigator.clipboard.writeText(copyLink);
|
|
2421
|
+
alert('复制链接已复制到剪贴板');
|
|
2422
|
+
} catch (error) {
|
|
2423
|
+
alert(`复制链接失败: ${error.message}`);
|
|
2424
|
+
}
|
|
2425
|
+
},
|
|
1682
2426
|
onDelete: (state.readonly || !state.fullAccess) ? undefined : async (id) => {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2427
|
+
const current = state.sandboxes.find((sandbox) => String(sandbox.id || '') === String(id || ''));
|
|
2428
|
+
if (!current) return;
|
|
2429
|
+
const expectedName = String(current.name || '').trim();
|
|
2430
|
+
const typedName = String(window.prompt(`为避免误删,请输入沙盘名称确认删除:${expectedName}`) || '').trim();
|
|
2431
|
+
if (!typedName) return;
|
|
2432
|
+
if (typedName !== expectedName) {
|
|
2433
|
+
alert('输入名称不匹配,已取消删除。');
|
|
2434
|
+
return;
|
|
1686
2435
|
}
|
|
2436
|
+
await apiRequest(`${API_BASE}/sandboxes/${id}`, { method: 'DELETE' });
|
|
2437
|
+
await loadSandboxes();
|
|
1687
2438
|
},
|
|
1688
2439
|
readonly: state.readonly || !state.fullAccess,
|
|
1689
2440
|
});
|
|
1690
2441
|
}
|
|
1691
2442
|
|
|
2443
|
+
function renderTaskBoardList(containerId, rows = []) {
|
|
2444
|
+
const container = document.getElementById(containerId);
|
|
2445
|
+
if (!(container instanceof HTMLElement)) return;
|
|
2446
|
+
if (!rows.length) {
|
|
2447
|
+
container.innerHTML = '<div class="empty-state"><p>暂无任务</p></div>';
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
container.innerHTML = rows.map((row) => `
|
|
2451
|
+
<article class="task-board-item">
|
|
2452
|
+
<div><strong>${safeText(row.name || '-')}</strong> <span class="entity-type-pill">${safeText(row.record_type || 'todo')}</span></div>
|
|
2453
|
+
<div class="task-board-item-meta">沙盘:${safeText(row.sandbox_name || row.sandbox_id || '-')}</div>
|
|
2454
|
+
<div class="task-board-item-meta">负责人:${safeText(row.assignee || '我(未指派)')} · 状态:${safeText(row.status || '-')}</div>
|
|
2455
|
+
<div class="task-board-item-meta">计划启动:${safeText(row.planned_start_date || '-')} · 截止:${safeText(row.due_date || '-')}</div>
|
|
2456
|
+
</article>
|
|
2457
|
+
`).join('');
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
function normalizeTaskBoardPeriod(value) {
|
|
2461
|
+
const normalized = String(value || '').trim();
|
|
2462
|
+
if (['week', 'month', 'quarter', 'all'].includes(normalized)) {
|
|
2463
|
+
return normalized;
|
|
2464
|
+
}
|
|
2465
|
+
return 'week';
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
function getTaskBoardRangeLabel(taskBoardData) {
|
|
2469
|
+
const period = normalizeTaskBoardPeriod(taskBoardData?.period || state.taskBoardPeriod);
|
|
2470
|
+
if (period === 'all') return '全部';
|
|
2471
|
+
return `${safeText(taskBoardData?.start_date || '-')} ~ ${safeText(taskBoardData?.end_date || '-')}`;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
function renderTaskBoard() {
|
|
2475
|
+
const summary = document.getElementById('task-board-summary');
|
|
2476
|
+
const periodSelect = document.getElementById('task-board-period');
|
|
2477
|
+
const ownerInput = document.getElementById('task-board-owner-input');
|
|
2478
|
+
if (periodSelect instanceof HTMLSelectElement) {
|
|
2479
|
+
periodSelect.value = normalizeTaskBoardPeriod(state.taskBoardPeriod);
|
|
2480
|
+
}
|
|
2481
|
+
if (ownerInput instanceof HTMLInputElement) {
|
|
2482
|
+
ownerInput.value = state.taskBoardOwnerName || '';
|
|
2483
|
+
}
|
|
2484
|
+
if (!state.taskBoardData) {
|
|
2485
|
+
renderTaskBoardList('task-board-mine-list', []);
|
|
2486
|
+
renderTaskBoardList('task-board-others-list', []);
|
|
2487
|
+
if (summary instanceof HTMLElement) {
|
|
2488
|
+
summary.innerHTML = '';
|
|
2489
|
+
}
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
renderTaskBoardList('task-board-mine-list', state.taskBoardData.mine || []);
|
|
2493
|
+
renderTaskBoardList('task-board-others-list', state.taskBoardData.others || []);
|
|
2494
|
+
if (summary instanceof HTMLElement) {
|
|
2495
|
+
const counts = state.taskBoardData.counts || { mine: 0, others: 0, total: 0 };
|
|
2496
|
+
summary.innerHTML = `
|
|
2497
|
+
<div class="summary-card"><div class="label">统计区间</div><div class="value" style="font-size:13px;font-weight:500">${getTaskBoardRangeLabel(state.taskBoardData)}</div></div>
|
|
2498
|
+
<div class="summary-card"><div class="label">我的任务</div><div class="value">${Number(counts.mine || 0)}</div></div>
|
|
2499
|
+
<div class="summary-card"><div class="label">他人任务</div><div class="value">${Number(counts.others || 0)}</div></div>
|
|
2500
|
+
<div class="summary-card"><div class="label">总数</div><div class="value">${Number(counts.total || 0)}</div></div>
|
|
2501
|
+
`;
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
async function loadTaskBoard() {
|
|
2506
|
+
const params = new URLSearchParams();
|
|
2507
|
+
params.set('period', normalizeTaskBoardPeriod(state.taskBoardPeriod));
|
|
2508
|
+
if (state.taskBoardOwnerName) {
|
|
2509
|
+
params.set('owner', state.taskBoardOwnerName);
|
|
2510
|
+
}
|
|
2511
|
+
state.taskBoardData = await apiRequest(`${API_BASE}/items/agenda?${params.toString()}`);
|
|
2512
|
+
renderTaskBoard();
|
|
2513
|
+
}
|
|
2514
|
+
|
|
1692
2515
|
async function loadSandbox(id) {
|
|
1693
2516
|
closeQuickChatPopover();
|
|
1694
2517
|
state.currentSandboxWritable = canWriteSandboxById(id);
|
|
@@ -2521,6 +3344,20 @@ function showPage(pageId) {
|
|
|
2521
3344
|
const navPageId = pageId === 'sandbox-detail' ? 'sandboxes' : pageId;
|
|
2522
3345
|
const nav = document.querySelector(`[data-nav="${navPageId}"]`);
|
|
2523
3346
|
if (nav) nav.classList.add('active');
|
|
3347
|
+
if (['ai-engineering', 'aser-runtime', 'agent-club', 'teams', 'opencode-team-runner'].includes(navPageId)) {
|
|
3348
|
+
setAiEngineeringNavCollapsed(false);
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
function setAiEngineeringNavCollapsed(collapsed) {
|
|
3353
|
+
const navGroup = document.getElementById('ai-engineering-nav-group');
|
|
3354
|
+
const toggleBtn = document.getElementById('toggle-ai-engineering-nav');
|
|
3355
|
+
if (!(navGroup instanceof HTMLElement)) return;
|
|
3356
|
+
navGroup.classList.toggle('nav-group-collapsed', Boolean(collapsed));
|
|
3357
|
+
if (toggleBtn instanceof HTMLButtonElement) {
|
|
3358
|
+
toggleBtn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
3359
|
+
toggleBtn.textContent = collapsed ? '▸' : '▾';
|
|
3360
|
+
}
|
|
2524
3361
|
}
|
|
2525
3362
|
|
|
2526
3363
|
function editWorkItem(id) {
|
|
@@ -2543,8 +3380,19 @@ async function initApp() {
|
|
|
2543
3380
|
await loadRuntimeMode();
|
|
2544
3381
|
loadWorkItemAssigneePreference();
|
|
2545
3382
|
loadWorkTreeViewModePreference();
|
|
3383
|
+
loadSandboxSortModePreference();
|
|
3384
|
+
loadTaskBoardOwnerNamePreference();
|
|
3385
|
+
loadAiEngineeringState();
|
|
2546
3386
|
renderQuickDiaryTargetLabel();
|
|
2547
3387
|
applyReadonlyMode();
|
|
3388
|
+
aserRuntimeView = createAserRuntimeView({
|
|
3389
|
+
readonly: state.readonly,
|
|
3390
|
+
hasAccess: state.fullAccess || state.pageAccess.sandboxes,
|
|
3391
|
+
getTeams: () => state.teams.map((team) => ({ ...team })),
|
|
3392
|
+
getProjectTeamAssignments: () => ({ ...(state.aserProjectTeamAssignments || {}) }),
|
|
3393
|
+
assignProjectTeam: (projectId, teamId) => assignAserProjectTeam(projectId, teamId),
|
|
3394
|
+
});
|
|
3395
|
+
aserRuntimeView.mount();
|
|
2548
3396
|
|
|
2549
3397
|
document.querySelectorAll('.nav-list a').forEach(link => {
|
|
2550
3398
|
link.addEventListener('click', (e) => {
|
|
@@ -2552,14 +3400,172 @@ async function initApp() {
|
|
|
2552
3400
|
window.location.hash = link.getAttribute('href').replace('#', '') || '/';
|
|
2553
3401
|
});
|
|
2554
3402
|
});
|
|
3403
|
+
|
|
3404
|
+
document.getElementById('agent-club-create-btn')?.addEventListener('click', () => {
|
|
3405
|
+
const nameInput = document.getElementById('agent-club-name');
|
|
3406
|
+
const roleInput = document.getElementById('agent-club-role');
|
|
3407
|
+
const knowledgeInput = document.getElementById('agent-club-knowledge');
|
|
3408
|
+
const skillsInput = document.getElementById('agent-club-skills');
|
|
3409
|
+
const taskTypesInput = document.getElementById('agent-club-task-types');
|
|
3410
|
+
const deliverablesInput = document.getElementById('agent-club-deliverables');
|
|
3411
|
+
const raciInput = document.getElementById('agent-club-raci-default');
|
|
3412
|
+
const qualityInput = document.getElementById('agent-club-quality-criteria');
|
|
3413
|
+
const riskInput = document.getElementById('agent-club-risk-limits');
|
|
3414
|
+
const name = String(nameInput?.value || '').trim();
|
|
3415
|
+
const role = String(roleInput?.value || '').trim();
|
|
3416
|
+
const knowledgeBackground = parseLineList(knowledgeInput?.value);
|
|
3417
|
+
const skills = parseSkillRows(skillsInput?.value);
|
|
3418
|
+
const taskTypes = parseLineList(taskTypesInput?.value);
|
|
3419
|
+
const deliverables = parseLineList(deliverablesInput?.value);
|
|
3420
|
+
const raciDefault = String(raciInput?.value || '').trim();
|
|
3421
|
+
const qualityCriteria = parseLineList(qualityInput?.value);
|
|
3422
|
+
const riskLimits = parseLineList(riskInput?.value);
|
|
3423
|
+
if (!name) {
|
|
3424
|
+
alert('请填写 Agent 名称');
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
const id = `agent-${Date.now()}`;
|
|
3428
|
+
state.agentClub.unshift(normalizeAgentRecord({
|
|
3429
|
+
id,
|
|
3430
|
+
name,
|
|
3431
|
+
role_type: role || 'generalist',
|
|
3432
|
+
knowledge_background: knowledgeBackground,
|
|
3433
|
+
skills,
|
|
3434
|
+
task_types: taskTypes,
|
|
3435
|
+
deliverables,
|
|
3436
|
+
raci_default: raciDefault,
|
|
3437
|
+
quality_criteria: qualityCriteria,
|
|
3438
|
+
risk_limits: riskLimits,
|
|
3439
|
+
}));
|
|
3440
|
+
persistAiEngineeringState();
|
|
3441
|
+
renderAgentClub();
|
|
3442
|
+
renderTeams();
|
|
3443
|
+
renderOpenCodeRunner();
|
|
3444
|
+
if (nameInput) nameInput.value = '';
|
|
3445
|
+
if (roleInput) roleInput.value = '';
|
|
3446
|
+
if (knowledgeInput) knowledgeInput.value = '';
|
|
3447
|
+
if (skillsInput) skillsInput.value = '';
|
|
3448
|
+
if (taskTypesInput) taskTypesInput.value = '';
|
|
3449
|
+
if (deliverablesInput) deliverablesInput.value = '';
|
|
3450
|
+
if (raciInput) raciInput.value = '';
|
|
3451
|
+
if (qualityInput) qualityInput.value = '';
|
|
3452
|
+
if (riskInput) riskInput.value = '';
|
|
3453
|
+
if (aserRuntimeView) aserRuntimeView.refreshTeamContext();
|
|
3454
|
+
});
|
|
3455
|
+
|
|
3456
|
+
document.getElementById('agent-club-list')?.addEventListener('click', (event) => {
|
|
3457
|
+
const target = event.target;
|
|
3458
|
+
if (!(target instanceof HTMLElement)) return;
|
|
3459
|
+
const deleteId = String(target.getAttribute('data-agent-delete-id') || '').trim();
|
|
3460
|
+
if (!deleteId) return;
|
|
3461
|
+
state.agentClub = state.agentClub.filter((agent) => String(agent.id) !== deleteId);
|
|
3462
|
+
state.teams = state.teams.map((team) => ({
|
|
3463
|
+
...team,
|
|
3464
|
+
agent_ids: team.agent_ids.filter((agentId) => String(agentId) !== deleteId),
|
|
3465
|
+
}));
|
|
3466
|
+
persistAiEngineeringState();
|
|
3467
|
+
renderAgentClub();
|
|
3468
|
+
renderTeams();
|
|
3469
|
+
renderOpenCodeRunner();
|
|
3470
|
+
if (aserRuntimeView) aserRuntimeView.refreshTeamContext();
|
|
3471
|
+
});
|
|
3472
|
+
|
|
3473
|
+
document.getElementById('teams-create-btn')?.addEventListener('click', () => {
|
|
3474
|
+
const nameInput = document.getElementById('teams-name');
|
|
3475
|
+
const agentSelect = document.getElementById('teams-agent-select');
|
|
3476
|
+
const name = String(nameInput?.value || '').trim();
|
|
3477
|
+
if (!name) {
|
|
3478
|
+
alert('请填写 Team 名称');
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
const agentIds = agentSelect instanceof HTMLSelectElement
|
|
3482
|
+
? Array.from(agentSelect.selectedOptions)
|
|
3483
|
+
.map((option) => String(option.value || '').trim())
|
|
3484
|
+
.filter(Boolean)
|
|
3485
|
+
.filter((agentId) => state.agentClub.some((agent) => String(agent.id) === agentId))
|
|
3486
|
+
: [];
|
|
3487
|
+
const id = `team-${Date.now()}`;
|
|
3488
|
+
state.teams.unshift(normalizeTeamRecord({ id, name, agent_ids: agentIds }));
|
|
3489
|
+
persistAiEngineeringState();
|
|
3490
|
+
renderTeams();
|
|
3491
|
+
renderOpenCodeRunner();
|
|
3492
|
+
if (nameInput) nameInput.value = '';
|
|
3493
|
+
if (agentSelect instanceof HTMLSelectElement) {
|
|
3494
|
+
Array.from(agentSelect.options).forEach((option) => {
|
|
3495
|
+
option.selected = false;
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
if (aserRuntimeView) aserRuntimeView.refreshTeamContext();
|
|
3499
|
+
});
|
|
3500
|
+
|
|
3501
|
+
document.getElementById('teams-list')?.addEventListener('click', (event) => {
|
|
3502
|
+
const target = event.target;
|
|
3503
|
+
if (!(target instanceof HTMLElement)) return;
|
|
3504
|
+
const deleteId = String(target.getAttribute('data-team-delete-id') || '').trim();
|
|
3505
|
+
if (!deleteId) return;
|
|
3506
|
+
state.teams = state.teams.filter((team) => String(team.id) !== deleteId);
|
|
3507
|
+
Object.keys(state.aserProjectTeamAssignments || {}).forEach((projectId) => {
|
|
3508
|
+
if (String(state.aserProjectTeamAssignments[projectId]) === deleteId) {
|
|
3509
|
+
delete state.aserProjectTeamAssignments[projectId];
|
|
3510
|
+
}
|
|
3511
|
+
});
|
|
3512
|
+
persistAiEngineeringState();
|
|
3513
|
+
renderTeams();
|
|
3514
|
+
renderOpenCodeRunner();
|
|
3515
|
+
if (aserRuntimeView) aserRuntimeView.refreshTeamContext();
|
|
3516
|
+
});
|
|
3517
|
+
|
|
3518
|
+
document.getElementById('opencode-runner-project-select')?.addEventListener('change', (event) => {
|
|
3519
|
+
const target = event.target;
|
|
3520
|
+
if (!(target instanceof HTMLSelectElement)) return;
|
|
3521
|
+
state.opencodeRunnerSelectedProjectId = String(target.value || '').trim();
|
|
3522
|
+
const assigned = getAssignedTeamIdForProject(state.opencodeRunnerSelectedProjectId);
|
|
3523
|
+
if (assigned) state.opencodeRunnerSelectedTeamId = assigned;
|
|
3524
|
+
renderOpenCodeRunner();
|
|
3525
|
+
});
|
|
3526
|
+
|
|
3527
|
+
document.getElementById('opencode-runner-team-select')?.addEventListener('change', (event) => {
|
|
3528
|
+
const target = event.target;
|
|
3529
|
+
if (!(target instanceof HTMLSelectElement)) return;
|
|
3530
|
+
state.opencodeRunnerSelectedTeamId = String(target.value || '').trim();
|
|
3531
|
+
renderOpenCodeRunner();
|
|
3532
|
+
});
|
|
3533
|
+
|
|
3534
|
+
document.getElementById('opencode-runner-load-btn')?.addEventListener('click', () => {
|
|
3535
|
+
loadOpenCodeRunnerTasks().catch((error) => {
|
|
3536
|
+
alert(`获取任务失败: ${error?.message || error}`);
|
|
3537
|
+
});
|
|
3538
|
+
});
|
|
3539
|
+
|
|
3540
|
+
document.getElementById('opencode-runner-plan-btn')?.addEventListener('click', () => {
|
|
3541
|
+
planOpenCodeRunnerTasks();
|
|
3542
|
+
});
|
|
3543
|
+
|
|
3544
|
+
document.getElementById('opencode-runner-start-btn')?.addEventListener('click', () => {
|
|
3545
|
+
startOpenCodeRunner().catch((error) => {
|
|
3546
|
+
alert(`启动 Runner 失败: ${error?.message || error}`);
|
|
3547
|
+
});
|
|
3548
|
+
});
|
|
2555
3549
|
|
|
2556
3550
|
document.getElementById('add-sandbox-btn')?.addEventListener('click', () => {
|
|
2557
3551
|
if (state.readonly) return;
|
|
2558
|
-
document.getElementById('sandbox-dialog')
|
|
3552
|
+
const dialog = document.getElementById('sandbox-dialog');
|
|
3553
|
+
if (!dialog) return;
|
|
3554
|
+
dialog.dataset.editId = '';
|
|
3555
|
+
const title = document.getElementById('sandbox-dialog-title');
|
|
3556
|
+
const confirmBtn = document.getElementById('confirm-sandbox-btn');
|
|
3557
|
+
if (title) title.textContent = '新建沙盘';
|
|
3558
|
+
if (confirmBtn) confirmBtn.textContent = '创建';
|
|
3559
|
+
document.getElementById('new-sandbox-name').value = '';
|
|
3560
|
+
document.getElementById('new-sandbox-desc').value = '';
|
|
3561
|
+
dialog.showModal();
|
|
2559
3562
|
});
|
|
2560
3563
|
|
|
2561
3564
|
document.getElementById('cancel-sandbox-btn')?.addEventListener('click', () => {
|
|
2562
|
-
document.getElementById('sandbox-dialog')
|
|
3565
|
+
const dialog = document.getElementById('sandbox-dialog');
|
|
3566
|
+
if (!dialog) return;
|
|
3567
|
+
dialog.dataset.editId = '';
|
|
3568
|
+
dialog.close();
|
|
2563
3569
|
});
|
|
2564
3570
|
|
|
2565
3571
|
const createSandbox = async (e) => {
|
|
@@ -2568,23 +3574,78 @@ async function initApp() {
|
|
|
2568
3574
|
const name = document.getElementById('new-sandbox-name').value;
|
|
2569
3575
|
const description = document.getElementById('new-sandbox-desc').value;
|
|
2570
3576
|
const dialog = document.getElementById('sandbox-dialog');
|
|
3577
|
+
const editId = String(dialog?.dataset?.editId || '').trim();
|
|
2571
3578
|
|
|
2572
3579
|
try {
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
3580
|
+
if (editId) {
|
|
3581
|
+
await apiRequest(`${API_BASE}/sandboxes/${editId}`, {
|
|
3582
|
+
method: 'PUT',
|
|
3583
|
+
body: JSON.stringify({ name, description }),
|
|
3584
|
+
});
|
|
3585
|
+
} else {
|
|
3586
|
+
await apiRequest(`${API_BASE}/sandboxes`, {
|
|
3587
|
+
method: 'POST',
|
|
3588
|
+
body: JSON.stringify({ name, description }),
|
|
3589
|
+
});
|
|
3590
|
+
}
|
|
3591
|
+
dialog.dataset.editId = '';
|
|
2577
3592
|
dialog.close();
|
|
2578
3593
|
document.getElementById('new-sandbox-name').value = '';
|
|
2579
3594
|
document.getElementById('new-sandbox-desc').value = '';
|
|
2580
3595
|
await loadSandboxes();
|
|
2581
3596
|
} catch (error) {
|
|
2582
|
-
alert('
|
|
3597
|
+
alert(`${editId ? '保存' : '创建'}失败: ${error.message}`);
|
|
2583
3598
|
}
|
|
2584
3599
|
};
|
|
2585
3600
|
|
|
2586
3601
|
document.getElementById('sandbox-dialog')?.addEventListener('submit', createSandbox);
|
|
2587
3602
|
document.getElementById('confirm-sandbox-btn')?.addEventListener('click', createSandbox);
|
|
3603
|
+
document.getElementById('sandbox-sort-mode')?.addEventListener('change', (event) => {
|
|
3604
|
+
const mode = String(event?.target?.value || '').trim();
|
|
3605
|
+
if (mode === 'created_asc' || mode === 'created_desc' || mode === 'updated_desc' || mode === 'name_asc') {
|
|
3606
|
+
state.sandboxSortMode = mode;
|
|
3607
|
+
persistSandboxSortModePreference();
|
|
3608
|
+
renderSandboxes();
|
|
3609
|
+
}
|
|
3610
|
+
});
|
|
3611
|
+
|
|
3612
|
+
document.getElementById('import-sandbox-btn')?.addEventListener('click', () => {
|
|
3613
|
+
if (state.readonly) return;
|
|
3614
|
+
const dialog = document.getElementById('import-sandbox-dialog');
|
|
3615
|
+
if (!dialog) return;
|
|
3616
|
+
document.getElementById('import-sandbox-link').value = '';
|
|
3617
|
+
document.getElementById('import-sandbox-name').value = '';
|
|
3618
|
+
dialog.showModal();
|
|
3619
|
+
});
|
|
3620
|
+
document.getElementById('cancel-import-sandbox-btn')?.addEventListener('click', () => {
|
|
3621
|
+
document.getElementById('import-sandbox-dialog')?.close();
|
|
3622
|
+
});
|
|
3623
|
+
const importSandboxByLink = async (e) => {
|
|
3624
|
+
if (state.readonly) return;
|
|
3625
|
+
e?.preventDefault?.();
|
|
3626
|
+
const dialog = document.getElementById('import-sandbox-dialog');
|
|
3627
|
+
const copyLink = String(document.getElementById('import-sandbox-link').value || '').trim();
|
|
3628
|
+
const name = String(document.getElementById('import-sandbox-name').value || '').trim();
|
|
3629
|
+
if (!copyLink) {
|
|
3630
|
+
alert('请先输入复制链接');
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3633
|
+
try {
|
|
3634
|
+
const created = await apiRequest(`${API_BASE}/sandboxes/import-from-link`, {
|
|
3635
|
+
method: 'POST',
|
|
3636
|
+
body: JSON.stringify({ copy_link: copyLink, name }),
|
|
3637
|
+
});
|
|
3638
|
+
dialog?.close();
|
|
3639
|
+
await loadSandboxes();
|
|
3640
|
+
if (created?.id) {
|
|
3641
|
+
window.location.hash = `/sandbox/${created.id}`;
|
|
3642
|
+
}
|
|
3643
|
+
} catch (error) {
|
|
3644
|
+
alert(`复制失败: ${error.message}`);
|
|
3645
|
+
}
|
|
3646
|
+
};
|
|
3647
|
+
document.getElementById('import-sandbox-dialog')?.addEventListener('submit', importSandboxByLink);
|
|
3648
|
+
document.getElementById('confirm-import-sandbox-btn')?.addEventListener('click', importSandboxByLink);
|
|
2588
3649
|
|
|
2589
3650
|
document.getElementById('add-item-btn')?.addEventListener('click', () => {
|
|
2590
3651
|
if (state.readonly) return;
|
|
@@ -2619,13 +3680,49 @@ async function initApp() {
|
|
|
2619
3680
|
document.getElementById('item-dialog').close();
|
|
2620
3681
|
});
|
|
2621
3682
|
|
|
3683
|
+
document.getElementById('toggle-ai-engineering-nav')?.addEventListener('click', () => {
|
|
3684
|
+
const navGroup = document.getElementById('ai-engineering-nav-group');
|
|
3685
|
+
if (!(navGroup instanceof HTMLElement)) return;
|
|
3686
|
+
const collapsed = navGroup.classList.contains('nav-group-collapsed');
|
|
3687
|
+
setAiEngineeringNavCollapsed(!collapsed);
|
|
3688
|
+
});
|
|
3689
|
+
|
|
3690
|
+
document.getElementById('task-board-period')?.addEventListener('change', async (event) => {
|
|
3691
|
+
const value = String(event?.target?.value || 'week');
|
|
3692
|
+
state.taskBoardPeriod = normalizeTaskBoardPeriod(value);
|
|
3693
|
+
await loadTaskBoard();
|
|
3694
|
+
});
|
|
3695
|
+
|
|
3696
|
+
document.getElementById('task-board-owner-input')?.addEventListener('change', async (event) => {
|
|
3697
|
+
state.taskBoardOwnerName = String(event?.target?.value || '').trim();
|
|
3698
|
+
persistTaskBoardOwnerNamePreference();
|
|
3699
|
+
await loadTaskBoard();
|
|
3700
|
+
});
|
|
3701
|
+
|
|
3702
|
+
document.getElementById('task-board-refresh-btn')?.addEventListener('click', async () => {
|
|
3703
|
+
await loadTaskBoard();
|
|
3704
|
+
});
|
|
3705
|
+
|
|
2622
3706
|
document.getElementById('close-node-drawer-btn')?.addEventListener('click', () => {
|
|
2623
3707
|
closeNodeEntityDrawer();
|
|
2624
3708
|
});
|
|
2625
3709
|
|
|
2626
3710
|
document.getElementById('toggle-node-entity-form-btn')?.addEventListener('click', () => {
|
|
2627
3711
|
if (state.readonly) return;
|
|
2628
|
-
|
|
3712
|
+
const nextExpanded = !state.nodeEntityFormExpanded;
|
|
3713
|
+
if (nextExpanded) {
|
|
3714
|
+
const preferredType = ['todo', 'issue', 'knowledge', 'capability'].includes(state.nodeEntityFilter)
|
|
3715
|
+
? state.nodeEntityFilter
|
|
3716
|
+
: 'issue';
|
|
3717
|
+
if (!state.editingNodeEntityId) {
|
|
3718
|
+
const typeSelect = document.getElementById('entity-type-select');
|
|
3719
|
+
if (typeSelect instanceof HTMLSelectElement) {
|
|
3720
|
+
typeSelect.value = preferredType;
|
|
3721
|
+
applyEntityFormMode();
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
setNodeEntityFormExpanded(nextExpanded);
|
|
2629
3726
|
if (state.nodeEntityFormExpanded) {
|
|
2630
3727
|
document.getElementById('entity-title-input')?.focus();
|
|
2631
3728
|
}
|
|
@@ -2660,31 +3757,77 @@ async function initApp() {
|
|
|
2660
3757
|
const entity_type = document.getElementById('entity-type-select').value;
|
|
2661
3758
|
if (!title) return;
|
|
2662
3759
|
const rawStatus = document.getElementById('entity-status-select').value || '';
|
|
2663
|
-
const status = rawStatus || (entity_type === '
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
status,
|
|
2671
|
-
priority: document.getElementById('entity-priority-select').value || '',
|
|
2672
|
-
capability_type: document.getElementById('entity-capability-type-input').value || '',
|
|
2673
|
-
};
|
|
2674
|
-
|
|
3760
|
+
const status = rawStatus || (entity_type === 'todo'
|
|
3761
|
+
? 'pending'
|
|
3762
|
+
: entity_type === 'issue'
|
|
3763
|
+
? 'open'
|
|
3764
|
+
: entity_type === 'capability'
|
|
3765
|
+
? 'building'
|
|
3766
|
+
: '');
|
|
2675
3767
|
const isEditing = Boolean(state.editingNodeEntityId);
|
|
3768
|
+
const editingTodoId = isEditing && String(state.editingNodeEntityId).startsWith('todo:')
|
|
3769
|
+
? String(state.editingNodeEntityId).slice(5)
|
|
3770
|
+
: '';
|
|
3771
|
+
const editingEntityId = isEditing && !editingTodoId ? String(state.editingNodeEntityId) : '';
|
|
3772
|
+
|
|
2676
3773
|
setButtonState(btn, { disabled: true, text: isEditing ? '保存中...' : '添加中...' });
|
|
2677
3774
|
try {
|
|
2678
|
-
if (
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
3775
|
+
if (entity_type === 'todo') {
|
|
3776
|
+
const todoPayload = {
|
|
3777
|
+
name: title,
|
|
3778
|
+
parent_id: state.selectedNodeId,
|
|
3779
|
+
description: '',
|
|
3780
|
+
assignee: document.getElementById('entity-assignee-input').value || '',
|
|
3781
|
+
status,
|
|
3782
|
+
priority: document.getElementById('entity-priority-select').value || 'medium',
|
|
3783
|
+
extra_data: {
|
|
3784
|
+
todo: {
|
|
3785
|
+
is_todo: true,
|
|
3786
|
+
planned_start_date: document.getElementById('entity-todo-planned-start-input').value || '',
|
|
3787
|
+
due_date: document.getElementById('entity-todo-due-date-input').value || '',
|
|
3788
|
+
},
|
|
3789
|
+
},
|
|
3790
|
+
};
|
|
3791
|
+
const plannedStart = String(todoPayload.extra_data.todo.planned_start_date || '').trim();
|
|
3792
|
+
const dueDate = String(todoPayload.extra_data.todo.due_date || '').trim();
|
|
3793
|
+
if (plannedStart && dueDate && plannedStart > dueDate) {
|
|
3794
|
+
alert('计划启动日期不能晚于截止日期。');
|
|
3795
|
+
return;
|
|
3796
|
+
}
|
|
3797
|
+
if (editingTodoId) {
|
|
3798
|
+
await apiRequest(`${API_BASE}/items/${editingTodoId}`, {
|
|
3799
|
+
method: 'PUT',
|
|
3800
|
+
body: JSON.stringify(todoPayload),
|
|
3801
|
+
});
|
|
3802
|
+
} else {
|
|
3803
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/items`, {
|
|
3804
|
+
method: 'POST',
|
|
3805
|
+
body: JSON.stringify(todoPayload),
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
2683
3808
|
} else {
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
3809
|
+
const payload = {
|
|
3810
|
+
work_item_id: state.selectedNodeId,
|
|
3811
|
+
entity_type,
|
|
3812
|
+
title,
|
|
3813
|
+
content_md: document.getElementById('entity-content-input').value || '',
|
|
3814
|
+
assignee: document.getElementById('entity-assignee-input').value || '',
|
|
3815
|
+
due_date: document.getElementById('entity-due-date-input').value || '',
|
|
3816
|
+
status,
|
|
3817
|
+
priority: document.getElementById('entity-priority-select').value || '',
|
|
3818
|
+
capability_type: document.getElementById('entity-capability-type-input').value || '',
|
|
3819
|
+
};
|
|
3820
|
+
if (editingEntityId) {
|
|
3821
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/entities/${editingEntityId}`, {
|
|
3822
|
+
method: 'PUT',
|
|
3823
|
+
body: JSON.stringify(payload),
|
|
3824
|
+
});
|
|
3825
|
+
} else {
|
|
3826
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/entities`, {
|
|
3827
|
+
method: 'POST',
|
|
3828
|
+
body: JSON.stringify(payload),
|
|
3829
|
+
});
|
|
3830
|
+
}
|
|
2688
3831
|
}
|
|
2689
3832
|
await loadSandbox(state.currentSandbox.id);
|
|
2690
3833
|
showNodeEntityDrawer(state.selectedNodeId);
|
|
@@ -2755,7 +3898,9 @@ async function initApp() {
|
|
|
2755
3898
|
if (state.readonly) return;
|
|
2756
3899
|
const dialog = document.getElementById('item-dialog');
|
|
2757
3900
|
const editId = dialog.dataset.editId || null;
|
|
2758
|
-
const
|
|
3901
|
+
const editingItem = editId
|
|
3902
|
+
? state.currentSandbox?.items?.find((item) => String(item.id) === String(editId))
|
|
3903
|
+
: null;
|
|
2759
3904
|
|
|
2760
3905
|
const data = {
|
|
2761
3906
|
name: document.getElementById('new-item-name').value,
|
|
@@ -2765,6 +3910,10 @@ async function initApp() {
|
|
|
2765
3910
|
priority: document.getElementById('new-item-priority').value,
|
|
2766
3911
|
parent_id: document.getElementById('new-item-parent').value || null,
|
|
2767
3912
|
};
|
|
3913
|
+
const mergedExtraData = {
|
|
3914
|
+
...(editingItem?.extra_data || {}),
|
|
3915
|
+
};
|
|
3916
|
+
data.extra_data = mergedExtraData;
|
|
2768
3917
|
|
|
2769
3918
|
if (!data.name) {
|
|
2770
3919
|
return;
|
|
@@ -3421,6 +4570,12 @@ async function initApp() {
|
|
|
3421
4570
|
} else if (state.canAccessSystemSettings) {
|
|
3422
4571
|
window.location.hash = '/system-settings';
|
|
3423
4572
|
}
|
|
4573
|
+
} else if (pathHash === '/ai-engineering') {
|
|
4574
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
4575
|
+
window.location.hash = '/';
|
|
4576
|
+
return;
|
|
4577
|
+
}
|
|
4578
|
+
showPage('ai-engineering');
|
|
3424
4579
|
} else if (pathHash === '/sandboxes') {
|
|
3425
4580
|
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
3426
4581
|
window.location.hash = '/';
|
|
@@ -3450,6 +4605,44 @@ async function initApp() {
|
|
|
3450
4605
|
showPage('diaries');
|
|
3451
4606
|
await loadDiaries();
|
|
3452
4607
|
await loadSandboxes();
|
|
4608
|
+
} else if (pathHash === '/tasks') {
|
|
4609
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
4610
|
+
window.location.hash = '/';
|
|
4611
|
+
return;
|
|
4612
|
+
}
|
|
4613
|
+
showPage('tasks');
|
|
4614
|
+
await loadTaskBoard();
|
|
4615
|
+
} else if (pathHash === '/aser-runtime') {
|
|
4616
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
4617
|
+
window.location.hash = '/';
|
|
4618
|
+
return;
|
|
4619
|
+
}
|
|
4620
|
+
showPage('aser-runtime');
|
|
4621
|
+
if (aserRuntimeView) {
|
|
4622
|
+
await aserRuntimeView.loadProjects();
|
|
4623
|
+
}
|
|
4624
|
+
} else if (pathHash === '/agent-club') {
|
|
4625
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
4626
|
+
window.location.hash = '/';
|
|
4627
|
+
return;
|
|
4628
|
+
}
|
|
4629
|
+
showPage('agent-club');
|
|
4630
|
+
renderAgentClub();
|
|
4631
|
+
} else if (pathHash === '/teams') {
|
|
4632
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
4633
|
+
window.location.hash = '/';
|
|
4634
|
+
return;
|
|
4635
|
+
}
|
|
4636
|
+
showPage('teams');
|
|
4637
|
+
renderTeams();
|
|
4638
|
+
} else if (pathHash === '/opencode-team-runner') {
|
|
4639
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
4640
|
+
window.location.hash = '/';
|
|
4641
|
+
return;
|
|
4642
|
+
}
|
|
4643
|
+
showPage('opencode-team-runner');
|
|
4644
|
+
await loadOpenCodeRunnerProjects();
|
|
4645
|
+
renderOpenCodeRunner();
|
|
3453
4646
|
} else if (pathHash === '/changes') {
|
|
3454
4647
|
if (!state.pageAccess.changes && !state.fullAccess) {
|
|
3455
4648
|
window.location.hash = '/';
|