@qnote/q-ai-note 1.0.13 → 1.0.15
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 +301 -0
- 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/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 +1200 -52
- package/dist/web/aserRuntimeView.js +2152 -0
- package/dist/web/index.html +348 -16
- package/dist/web/styles.css +1534 -137
- package/dist/web/vueRenderers.js +25 -16
- package/package.json +4 -2
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: [],
|
|
@@ -39,6 +40,9 @@ const state = {
|
|
|
39
40
|
sandboxSortMode: 'created_asc',
|
|
40
41
|
workItemSearch: '',
|
|
41
42
|
workItemStatusFilter: 'all',
|
|
43
|
+
taskBoardPeriod: 'week',
|
|
44
|
+
taskBoardOwnerName: '',
|
|
45
|
+
taskBoardData: null,
|
|
42
46
|
diarySearch: '',
|
|
43
47
|
diarySandboxFilter: '',
|
|
44
48
|
diaryProcessedFilter: 'all',
|
|
@@ -47,6 +51,16 @@ const state = {
|
|
|
47
51
|
changesTypeFilter: 'all',
|
|
48
52
|
changesQuickFilter: 'all',
|
|
49
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,
|
|
50
64
|
};
|
|
51
65
|
|
|
52
66
|
const expandedNodes = new Set();
|
|
@@ -54,9 +68,14 @@ let quickChatPopover = null;
|
|
|
54
68
|
let sandboxActionHandler = null;
|
|
55
69
|
let sandboxEscLocked = false;
|
|
56
70
|
let resizeRenderTimer = null;
|
|
71
|
+
let aserRuntimeView = null;
|
|
57
72
|
const WORK_ITEM_SHOW_ASSIGNEE_STORAGE_KEY = 'q-ai-note.work-item.show-assignee';
|
|
58
73
|
const WORK_TREE_VIEW_MODE_STORAGE_KEY = 'q-ai-note.work-tree.view-mode';
|
|
59
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';
|
|
60
79
|
|
|
61
80
|
function loadWorkItemAssigneePreference() {
|
|
62
81
|
try {
|
|
@@ -118,6 +137,509 @@ function persistSandboxSortModePreference() {
|
|
|
118
137
|
}
|
|
119
138
|
}
|
|
120
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
|
+
|
|
121
643
|
function compareSandboxesBySortMode(a, b) {
|
|
122
644
|
const mode = String(state.sandboxSortMode || 'created_asc');
|
|
123
645
|
const createdA = String(a?.created_at || '');
|
|
@@ -222,10 +744,34 @@ function applyReadonlyMode() {
|
|
|
222
744
|
if (diariesNav instanceof HTMLElement) {
|
|
223
745
|
diariesNav.classList.toggle('hidden', !state.pageAccess.diaries && !state.fullAccess);
|
|
224
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
|
+
}
|
|
225
751
|
const changesNav = document.querySelector('[data-nav="changes"]');
|
|
226
752
|
if (changesNav instanceof HTMLElement) {
|
|
227
753
|
changesNav.classList.toggle('hidden', !state.pageAccess.changes && !state.fullAccess);
|
|
228
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
|
+
}
|
|
229
775
|
const projectSettingsNav = document.querySelector('[data-nav="settings"]');
|
|
230
776
|
if (projectSettingsNav instanceof HTMLElement) {
|
|
231
777
|
projectSettingsNav.classList.toggle('hidden', !state.pageAccess.settings);
|
|
@@ -253,6 +799,12 @@ function applyReadonlyMode() {
|
|
|
253
799
|
closeQuickChatPopover();
|
|
254
800
|
}
|
|
255
801
|
applySandboxChatVisibility();
|
|
802
|
+
if (aserRuntimeView) {
|
|
803
|
+
aserRuntimeView.setAccessContext({
|
|
804
|
+
readonly: state.readonly,
|
|
805
|
+
hasAccess: state.fullAccess || state.pageAccess.sandboxes,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
256
808
|
}
|
|
257
809
|
|
|
258
810
|
function getSandboxLayoutElement() {
|
|
@@ -828,6 +1380,25 @@ function populateParentSelect(items, preferredParentId = null) {
|
|
|
828
1380
|
select.value = hasExpectedValue ? expectedValue : '';
|
|
829
1381
|
}
|
|
830
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
|
+
|
|
831
1402
|
function normalizeAssistantResponseText(raw) {
|
|
832
1403
|
let text = String(raw || '').trim();
|
|
833
1404
|
if (!text) return '';
|
|
@@ -974,15 +1545,26 @@ function ensureMermaidRuntime() {
|
|
|
974
1545
|
}
|
|
975
1546
|
if (mermaidLoadPromise) return mermaidLoadPromise;
|
|
976
1547
|
mermaidLoadPromise = new Promise((resolve, reject) => {
|
|
1548
|
+
const timeoutId = window.setTimeout(() => {
|
|
1549
|
+
reject(new Error('Mermaid script load timeout'));
|
|
1550
|
+
}, 2000);
|
|
1551
|
+
const finalizeResolve = () => {
|
|
1552
|
+
window.clearTimeout(timeoutId);
|
|
1553
|
+
resolve(window.mermaid);
|
|
1554
|
+
};
|
|
1555
|
+
const finalizeReject = (error) => {
|
|
1556
|
+
window.clearTimeout(timeoutId);
|
|
1557
|
+
reject(error);
|
|
1558
|
+
};
|
|
977
1559
|
const existing = document.querySelector('script[data-mermaid-runtime="1"]');
|
|
978
1560
|
if (existing) {
|
|
979
1561
|
existing.addEventListener('load', () => {
|
|
980
1562
|
if (window.mermaid && typeof window.mermaid.initialize === 'function') {
|
|
981
1563
|
window.mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' });
|
|
982
1564
|
}
|
|
983
|
-
|
|
1565
|
+
finalizeResolve();
|
|
984
1566
|
}, { once: true });
|
|
985
|
-
existing.addEventListener('error', () =>
|
|
1567
|
+
existing.addEventListener('error', () => finalizeReject(new Error('Mermaid script load failed')), { once: true });
|
|
986
1568
|
return;
|
|
987
1569
|
}
|
|
988
1570
|
const script = document.createElement('script');
|
|
@@ -993,11 +1575,15 @@ function ensureMermaidRuntime() {
|
|
|
993
1575
|
if (window.mermaid && typeof window.mermaid.initialize === 'function') {
|
|
994
1576
|
window.mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' });
|
|
995
1577
|
}
|
|
996
|
-
|
|
1578
|
+
finalizeResolve();
|
|
997
1579
|
}, { once: true });
|
|
998
|
-
script.addEventListener('error', () =>
|
|
1580
|
+
script.addEventListener('error', () => finalizeReject(new Error('Mermaid script load failed')), { once: true });
|
|
999
1581
|
document.head.appendChild(script);
|
|
1000
1582
|
});
|
|
1583
|
+
mermaidLoadPromise = mermaidLoadPromise.catch((error) => {
|
|
1584
|
+
mermaidLoadPromise = null;
|
|
1585
|
+
throw error;
|
|
1586
|
+
});
|
|
1001
1587
|
return mermaidLoadPromise;
|
|
1002
1588
|
}
|
|
1003
1589
|
|
|
@@ -1162,6 +1748,14 @@ function getNodeDiariesByNodeId(nodeId) {
|
|
|
1162
1748
|
return (state.currentSandbox?.diaries || []).filter((row) => row.work_item_id === nodeId);
|
|
1163
1749
|
}
|
|
1164
1750
|
|
|
1751
|
+
function getEntityTypeLabel(type) {
|
|
1752
|
+
const normalized = String(type || '').trim().toLowerCase();
|
|
1753
|
+
if (normalized === 'issue') return '问题';
|
|
1754
|
+
if (normalized === 'knowledge') return '知识';
|
|
1755
|
+
if (normalized === 'capability') return '能力';
|
|
1756
|
+
return String(type || '');
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1165
1759
|
function closeNodeEntityDrawer() {
|
|
1166
1760
|
const drawer = document.getElementById('node-entity-drawer');
|
|
1167
1761
|
if (!drawer) return;
|
|
@@ -1202,12 +1796,14 @@ function renderNodeEntitySummary(nodeId) {
|
|
|
1202
1796
|
const issues = rows.filter((row) => row.entity_type === 'issue');
|
|
1203
1797
|
const knowledges = rows.filter((row) => row.entity_type === 'knowledge');
|
|
1204
1798
|
const capabilities = rows.filter((row) => row.entity_type === 'capability');
|
|
1799
|
+
const todos = getNodeTodoItems(nodeId);
|
|
1205
1800
|
const openIssues = issues.filter((row) => row.status === 'open' || row.status === 'in_progress' || row.status === 'blocked').length;
|
|
1206
1801
|
container.innerHTML = `
|
|
1207
|
-
<div class="summary-card"><div class="label"
|
|
1208
|
-
<div class="summary-card"><div class="label"
|
|
1209
|
-
<div class="summary-card"><div class="label"
|
|
1210
|
-
<div class="summary-card"><div class="label"
|
|
1802
|
+
<div class="summary-card"><div class="label">问题</div><div class="value">${issues.length}</div></div>
|
|
1803
|
+
<div class="summary-card"><div class="label">开放问题</div><div class="value">${openIssues}</div></div>
|
|
1804
|
+
<div class="summary-card"><div class="label">知识</div><div class="value">${knowledges.length}</div></div>
|
|
1805
|
+
<div class="summary-card"><div class="label">能力</div><div class="value">${capabilities.length}</div></div>
|
|
1806
|
+
<div class="summary-card"><div class="label">Todo</div><div class="value">${todos.length}</div></div>
|
|
1211
1807
|
<div class="summary-card"><div class="label">Diary</div><div class="value">${diaries.length}</div></div>
|
|
1212
1808
|
`;
|
|
1213
1809
|
}
|
|
@@ -1232,8 +1828,10 @@ function renderNodeEntityList(nodeId) {
|
|
|
1232
1828
|
if (!container) return;
|
|
1233
1829
|
const allEntityRows = getNodeEntitiesByNodeId(nodeId);
|
|
1234
1830
|
const allDiaryRows = getNodeDiariesByNodeId(nodeId);
|
|
1831
|
+
const allTodoRows = getNodeTodoItems(nodeId);
|
|
1235
1832
|
const timelineRows = [
|
|
1236
1833
|
...allEntityRows.map((row) => ({ ...row, timeline_type: row.entity_type || 'issue' })),
|
|
1834
|
+
...allTodoRows.map((row) => ({ ...row, timeline_type: 'todo' })),
|
|
1237
1835
|
...allDiaryRows.map((row) => ({ ...row, timeline_type: 'diary' })),
|
|
1238
1836
|
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
1239
1837
|
const rows = state.nodeEntityFilter === 'all'
|
|
@@ -1270,11 +1868,37 @@ function renderNodeEntityList(nodeId) {
|
|
|
1270
1868
|
</div>
|
|
1271
1869
|
`;
|
|
1272
1870
|
}
|
|
1871
|
+
if (row.timeline_type === 'todo') {
|
|
1872
|
+
const todoMeta = getTodoMetaFromItem(row);
|
|
1873
|
+
return `
|
|
1874
|
+
<div class="entity-card">
|
|
1875
|
+
<div class="entity-card-header">
|
|
1876
|
+
<div>
|
|
1877
|
+
<span class="entity-type-pill">todo</span>
|
|
1878
|
+
<strong>${safeText(row.name || '-')}</strong>
|
|
1879
|
+
</div>
|
|
1880
|
+
${state.readonly ? '' : `
|
|
1881
|
+
<div class="entity-card-actions">
|
|
1882
|
+
<button class="btn btn-secondary btn-sm" data-todo-edit-id="${safeText(row.id)}">编辑</button>
|
|
1883
|
+
<button class="btn btn-secondary btn-sm" data-todo-delete-id="${safeText(row.id)}">删除</button>
|
|
1884
|
+
</div>
|
|
1885
|
+
`}
|
|
1886
|
+
</div>
|
|
1887
|
+
<div class="entity-meta">
|
|
1888
|
+
${safeText(new Date(row.created_at).toLocaleString())}
|
|
1889
|
+
${row.status ? ` · <span class="entity-status-pill ${safeText(row.status)}">${safeText(row.status)}</span>` : ''}
|
|
1890
|
+
${row.assignee ? ` · @${safeText(row.assignee)}` : ''}
|
|
1891
|
+
${todoMeta.plannedStartDate ? ` · 启动 ${safeText(todoMeta.plannedStartDate)}` : ''}
|
|
1892
|
+
${todoMeta.dueDate ? ` · 截止 ${safeText(todoMeta.dueDate)}` : ''}
|
|
1893
|
+
</div>
|
|
1894
|
+
</div>
|
|
1895
|
+
`;
|
|
1896
|
+
}
|
|
1273
1897
|
return `
|
|
1274
1898
|
<div class="entity-card">
|
|
1275
1899
|
<div class="entity-card-header">
|
|
1276
1900
|
<div>
|
|
1277
|
-
<span class="entity-type-pill">${safeText(row.entity_type)}</span>
|
|
1901
|
+
<span class="entity-type-pill">${safeText(getEntityTypeLabel(row.entity_type))}</span>
|
|
1278
1902
|
<strong>${safeText(row.title || '-')}</strong>
|
|
1279
1903
|
</div>
|
|
1280
1904
|
${state.readonly ? '' : `
|
|
@@ -1289,6 +1913,7 @@ function renderNodeEntityList(nodeId) {
|
|
|
1289
1913
|
${row.status ? ` · <span class="entity-status-pill ${safeText(row.status)}">${safeText(row.status)}</span>` : ''}
|
|
1290
1914
|
${row.priority ? ` · ${safeText(row.priority)}` : ''}
|
|
1291
1915
|
${row.assignee ? ` · @${safeText(row.assignee)}` : ''}
|
|
1916
|
+
${row.due_date ? ` · 截止 ${safeText(row.due_date)}` : ''}
|
|
1292
1917
|
</div>
|
|
1293
1918
|
<div class="entity-content">${renderMarkdownSnippet(row.content_md || '')}</div>
|
|
1294
1919
|
</div>
|
|
@@ -1316,6 +1941,41 @@ function renderNodeEntityList(nodeId) {
|
|
|
1316
1941
|
});
|
|
1317
1942
|
});
|
|
1318
1943
|
|
|
1944
|
+
container.querySelectorAll('[data-todo-edit-id]').forEach((el) => {
|
|
1945
|
+
el.addEventListener('click', (e) => {
|
|
1946
|
+
e.preventDefault();
|
|
1947
|
+
const id = el.getAttribute('data-todo-edit-id');
|
|
1948
|
+
if (!id) return;
|
|
1949
|
+
const row = allTodoRows.find((item) => item.id === id);
|
|
1950
|
+
if (!row) return;
|
|
1951
|
+
startEditNodeEntity({
|
|
1952
|
+
id: row.id,
|
|
1953
|
+
entity_type: 'todo',
|
|
1954
|
+
title: row.name,
|
|
1955
|
+
content_md: row.description || '',
|
|
1956
|
+
assignee: row.assignee || '',
|
|
1957
|
+
status: row.status || 'pending',
|
|
1958
|
+
priority: row.priority || 'medium',
|
|
1959
|
+
capability_type: '',
|
|
1960
|
+
due_date: '',
|
|
1961
|
+
todo_planned_start_date: getTodoMetaFromItem(row).plannedStartDate,
|
|
1962
|
+
todo_due_date: getTodoMetaFromItem(row).dueDate,
|
|
1963
|
+
});
|
|
1964
|
+
});
|
|
1965
|
+
});
|
|
1966
|
+
container.querySelectorAll('[data-todo-delete-id]').forEach((el) => {
|
|
1967
|
+
el.addEventListener('click', async (e) => {
|
|
1968
|
+
e.preventDefault();
|
|
1969
|
+
if (!state.currentSandbox) return;
|
|
1970
|
+
const id = String(el.getAttribute('data-todo-delete-id') || '');
|
|
1971
|
+
if (!id) return;
|
|
1972
|
+
if (!confirm('确定删除该待办?')) return;
|
|
1973
|
+
await apiRequest(`${API_BASE}/items/${id}`, { method: 'DELETE' });
|
|
1974
|
+
await loadSandbox(state.currentSandbox.id);
|
|
1975
|
+
if (state.selectedNodeId) showNodeEntityDrawer(state.selectedNodeId, state.nodeEntityFilter);
|
|
1976
|
+
});
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1319
1979
|
container.querySelectorAll('[data-diary-edit-id]').forEach((el) => {
|
|
1320
1980
|
el.addEventListener('click', (e) => {
|
|
1321
1981
|
e.preventDefault();
|
|
@@ -1352,6 +2012,7 @@ function resetNodeEntityForm() {
|
|
|
1352
2012
|
const titleInput = document.getElementById('entity-title-input');
|
|
1353
2013
|
const contentInput = document.getElementById('entity-content-input');
|
|
1354
2014
|
const assigneeInput = document.getElementById('entity-assignee-input');
|
|
2015
|
+
const dueDateInput = document.getElementById('entity-due-date-input');
|
|
1355
2016
|
const statusInput = document.getElementById('entity-status-select');
|
|
1356
2017
|
const priorityInput = document.getElementById('entity-priority-select');
|
|
1357
2018
|
const capabilityTypeInput = document.getElementById('entity-capability-type-input');
|
|
@@ -1359,6 +2020,11 @@ function resetNodeEntityForm() {
|
|
|
1359
2020
|
if (titleInput) titleInput.value = '';
|
|
1360
2021
|
if (contentInput) contentInput.value = '';
|
|
1361
2022
|
if (assigneeInput) assigneeInput.value = '';
|
|
2023
|
+
if (dueDateInput) dueDateInput.value = '';
|
|
2024
|
+
const todoPlannedStartInput = document.getElementById('entity-todo-planned-start-input');
|
|
2025
|
+
const todoDueDateInput = document.getElementById('entity-todo-due-date-input');
|
|
2026
|
+
if (todoPlannedStartInput) todoPlannedStartInput.value = '';
|
|
2027
|
+
if (todoDueDateInput) todoDueDateInput.value = '';
|
|
1362
2028
|
if (statusInput) statusInput.value = '';
|
|
1363
2029
|
if (priorityInput) priorityInput.value = '';
|
|
1364
2030
|
if (capabilityTypeInput) capabilityTypeInput.value = '';
|
|
@@ -1385,20 +2051,27 @@ function ensureCapabilityTypeOption(value) {
|
|
|
1385
2051
|
}
|
|
1386
2052
|
|
|
1387
2053
|
function startEditNodeEntity(row) {
|
|
1388
|
-
|
|
2054
|
+
const rowType = String(row.entity_type || 'issue');
|
|
2055
|
+
state.editingNodeEntityId = rowType === 'todo' ? `todo:${row.id}` : row.id;
|
|
1389
2056
|
setNodeEntityFormExpanded(true);
|
|
1390
2057
|
const typeInput = document.getElementById('entity-type-select');
|
|
1391
2058
|
const titleInput = document.getElementById('entity-title-input');
|
|
1392
2059
|
const contentInput = document.getElementById('entity-content-input');
|
|
1393
2060
|
const assigneeInput = document.getElementById('entity-assignee-input');
|
|
2061
|
+
const dueDateInput = document.getElementById('entity-due-date-input');
|
|
1394
2062
|
const statusInput = document.getElementById('entity-status-select');
|
|
1395
2063
|
const priorityInput = document.getElementById('entity-priority-select');
|
|
1396
2064
|
const capabilityTypeInput = document.getElementById('entity-capability-type-input');
|
|
1397
|
-
if (typeInput) typeInput.value =
|
|
2065
|
+
if (typeInput) typeInput.value = rowType || 'todo';
|
|
1398
2066
|
applyEntityFormMode();
|
|
1399
|
-
if (titleInput) titleInput.value = row.title || '';
|
|
2067
|
+
if (titleInput) titleInput.value = rowType === 'todo' ? (row.title || row.name || '') : (row.title || '');
|
|
1400
2068
|
if (contentInput) contentInput.value = row.content_md || '';
|
|
1401
2069
|
if (assigneeInput) assigneeInput.value = row.assignee || '';
|
|
2070
|
+
if (dueDateInput) dueDateInput.value = row.due_date || '';
|
|
2071
|
+
const todoPlannedStartInput = document.getElementById('entity-todo-planned-start-input');
|
|
2072
|
+
const todoDueDateInput = document.getElementById('entity-todo-due-date-input');
|
|
2073
|
+
if (todoPlannedStartInput) todoPlannedStartInput.value = row.todo_planned_start_date || '';
|
|
2074
|
+
if (todoDueDateInput) todoDueDateInput.value = row.todo_due_date || '';
|
|
1402
2075
|
if (statusInput) statusInput.value = row.status || '';
|
|
1403
2076
|
if (priorityInput) priorityInput.value = row.priority || '';
|
|
1404
2077
|
ensureCapabilityTypeOption(row.capability_type || '');
|
|
@@ -1417,7 +2090,7 @@ function showNodeEntityDrawer(nodeId, preferredFilter = 'all') {
|
|
|
1417
2090
|
const node = getNodeById(nodeId);
|
|
1418
2091
|
if (!drawer || !title || !node) return;
|
|
1419
2092
|
state.selectedNodeId = nodeId;
|
|
1420
|
-
const filter = ['all', 'issue', 'knowledge', 'capability', 'diary'].includes(preferredFilter) ? preferredFilter : 'all';
|
|
2093
|
+
const filter = ['all', 'todo', 'issue', 'knowledge', 'capability', 'diary'].includes(preferredFilter) ? preferredFilter : 'all';
|
|
1421
2094
|
state.nodeEntityFilter = filter;
|
|
1422
2095
|
title.textContent = node.name || nodeId;
|
|
1423
2096
|
renderNodeEntitySummary(nodeId);
|
|
@@ -1440,9 +2113,17 @@ function renderNodeEntityFilterTabs() {
|
|
|
1440
2113
|
}
|
|
1441
2114
|
|
|
1442
2115
|
function getStatusOptionsByEntityType(entityType) {
|
|
2116
|
+
if (entityType === 'todo') {
|
|
2117
|
+
return [
|
|
2118
|
+
{ value: 'pending', label: 'pending' },
|
|
2119
|
+
{ value: 'in_progress', label: 'in_progress' },
|
|
2120
|
+
{ value: 'done', label: 'done' },
|
|
2121
|
+
{ value: 'archived', label: 'archived' },
|
|
2122
|
+
];
|
|
2123
|
+
}
|
|
1443
2124
|
if (entityType === 'issue') {
|
|
1444
2125
|
return [
|
|
1445
|
-
{ value: '', label: '
|
|
2126
|
+
{ value: '', label: '状态(问题,默认 open)' },
|
|
1446
2127
|
{ value: 'open', label: 'open' },
|
|
1447
2128
|
{ value: 'in_progress', label: 'in_progress' },
|
|
1448
2129
|
{ value: 'blocked', label: 'blocked' },
|
|
@@ -1452,22 +2133,25 @@ function getStatusOptionsByEntityType(entityType) {
|
|
|
1452
2133
|
}
|
|
1453
2134
|
if (entityType === 'capability') {
|
|
1454
2135
|
return [
|
|
1455
|
-
{ value: '', label: '
|
|
2136
|
+
{ value: '', label: '状态(能力,默认 building)' },
|
|
1456
2137
|
{ value: 'building', label: 'building' },
|
|
1457
2138
|
{ value: 'ready', label: 'ready' },
|
|
1458
2139
|
];
|
|
1459
2140
|
}
|
|
1460
|
-
return [{ value: '', label: '
|
|
2141
|
+
return [{ value: '', label: '无状态(知识)' }];
|
|
1461
2142
|
}
|
|
1462
2143
|
|
|
1463
2144
|
function applyEntityFormMode() {
|
|
1464
2145
|
const type = document.getElementById('entity-type-select')?.value || 'issue';
|
|
1465
2146
|
const assigneeGroup = document.getElementById('entity-assignee-group');
|
|
2147
|
+
const dueDateGroup = document.getElementById('entity-due-date-group');
|
|
2148
|
+
const todoDateGroup = document.getElementById('entity-todo-date-group');
|
|
1466
2149
|
const statusGroup = document.getElementById('entity-status-group');
|
|
1467
2150
|
const priorityGroup = document.getElementById('entity-priority-group');
|
|
1468
2151
|
const capabilityTypeGroup = document.getElementById('entity-capability-type-group');
|
|
1469
2152
|
const hint = document.getElementById('entity-form-hint');
|
|
1470
2153
|
const statusSelect = document.getElementById('entity-status-select');
|
|
2154
|
+
const contentInput = document.getElementById('entity-content-input');
|
|
1471
2155
|
|
|
1472
2156
|
if (statusSelect) {
|
|
1473
2157
|
statusSelect.innerHTML = getStatusOptionsByEntityType(type)
|
|
@@ -1475,17 +2159,22 @@ function applyEntityFormMode() {
|
|
|
1475
2159
|
.join('');
|
|
1476
2160
|
}
|
|
1477
2161
|
|
|
1478
|
-
assigneeGroup?.classList.toggle('hidden', type
|
|
1479
|
-
|
|
2162
|
+
assigneeGroup?.classList.toggle('hidden', !(type === 'issue' || type === 'todo'));
|
|
2163
|
+
dueDateGroup?.classList.toggle('hidden', type !== 'issue');
|
|
2164
|
+
todoDateGroup?.classList.toggle('hidden', type !== 'todo');
|
|
2165
|
+
priorityGroup?.classList.toggle('hidden', !(type === 'issue' || type === 'todo'));
|
|
1480
2166
|
capabilityTypeGroup?.classList.toggle('hidden', type !== 'capability');
|
|
1481
2167
|
statusGroup?.classList.toggle('hidden', false);
|
|
2168
|
+
contentInput?.classList.toggle('hidden', type === 'todo');
|
|
1482
2169
|
|
|
1483
2170
|
if (hint) {
|
|
1484
|
-
hint.textContent = type === '
|
|
1485
|
-
? '
|
|
1486
|
-
: type === '
|
|
1487
|
-
? '
|
|
1488
|
-
:
|
|
2171
|
+
hint.textContent = type === 'todo'
|
|
2172
|
+
? 'Todo 将作为当前节点子任务创建。'
|
|
2173
|
+
: type === 'issue'
|
|
2174
|
+
? '问题推荐填写状态/优先级/负责人。'
|
|
2175
|
+
: type === 'knowledge'
|
|
2176
|
+
? '知识可仅保存链接或少量 Markdown。'
|
|
2177
|
+
: '能力建议填写能力类型与简述。';
|
|
1489
2178
|
}
|
|
1490
2179
|
}
|
|
1491
2180
|
|
|
@@ -1732,7 +2421,7 @@ function renderSandboxes() {
|
|
|
1732
2421
|
onOpen: (id) => {
|
|
1733
2422
|
window.location.hash = `/sandbox/${id}`;
|
|
1734
2423
|
},
|
|
1735
|
-
onEdit: (
|
|
2424
|
+
onEdit: (id) => {
|
|
1736
2425
|
const current = state.sandboxes.find((sandbox) => sandbox.id === id);
|
|
1737
2426
|
if (!current) return;
|
|
1738
2427
|
const dialog = document.getElementById('sandbox-dialog');
|
|
@@ -1746,7 +2435,7 @@ function renderSandboxes() {
|
|
|
1746
2435
|
document.getElementById('new-sandbox-desc').value = String(current.description || '');
|
|
1747
2436
|
dialog.showModal();
|
|
1748
2437
|
},
|
|
1749
|
-
onCopyLink:
|
|
2438
|
+
onCopyLink: async (id) => {
|
|
1750
2439
|
try {
|
|
1751
2440
|
const data = await apiRequest(`${API_BASE}/sandboxes/${id}/copy-link`, { method: 'POST' });
|
|
1752
2441
|
const copyLink = String(data?.copy_link || '');
|
|
@@ -1757,7 +2446,7 @@ function renderSandboxes() {
|
|
|
1757
2446
|
alert(`复制链接失败: ${error.message}`);
|
|
1758
2447
|
}
|
|
1759
2448
|
},
|
|
1760
|
-
onDelete:
|
|
2449
|
+
onDelete: async (id) => {
|
|
1761
2450
|
const current = state.sandboxes.find((sandbox) => String(sandbox.id || '') === String(id || ''));
|
|
1762
2451
|
if (!current) return;
|
|
1763
2452
|
const expectedName = String(current.name || '').trim();
|
|
@@ -1770,10 +2459,85 @@ function renderSandboxes() {
|
|
|
1770
2459
|
await apiRequest(`${API_BASE}/sandboxes/${id}`, { method: 'DELETE' });
|
|
1771
2460
|
await loadSandboxes();
|
|
1772
2461
|
},
|
|
1773
|
-
|
|
2462
|
+
canEdit: (sandbox) => canWriteSandboxById(sandbox?.id),
|
|
2463
|
+
canDelete: (sandbox) => canWriteSandboxById(sandbox?.id),
|
|
2464
|
+
canCopyLink: (sandbox) => canReadSandboxById(sandbox?.id),
|
|
2465
|
+
readonly: state.readonly,
|
|
1774
2466
|
});
|
|
1775
2467
|
}
|
|
1776
2468
|
|
|
2469
|
+
function renderTaskBoardList(containerId, rows = []) {
|
|
2470
|
+
const container = document.getElementById(containerId);
|
|
2471
|
+
if (!(container instanceof HTMLElement)) return;
|
|
2472
|
+
if (!rows.length) {
|
|
2473
|
+
container.innerHTML = '<div class="empty-state"><p>暂无任务</p></div>';
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
container.innerHTML = rows.map((row) => `
|
|
2477
|
+
<article class="task-board-item">
|
|
2478
|
+
<div><strong>${safeText(row.name || '-')}</strong> <span class="entity-type-pill">${safeText(getEntityTypeLabel(row.record_type || 'todo'))}</span></div>
|
|
2479
|
+
<div class="task-board-item-meta">沙盘:${safeText(row.sandbox_name || row.sandbox_id || '-')}</div>
|
|
2480
|
+
<div class="task-board-item-meta">负责人:${safeText(row.assignee || '我(未指派)')} · 状态:${safeText(row.status || '-')}</div>
|
|
2481
|
+
<div class="task-board-item-meta">计划启动:${safeText(row.planned_start_date || '-')} · 截止:${safeText(row.due_date || '-')}</div>
|
|
2482
|
+
</article>
|
|
2483
|
+
`).join('');
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
function normalizeTaskBoardPeriod(value) {
|
|
2487
|
+
const normalized = String(value || '').trim();
|
|
2488
|
+
if (['week', 'month', 'quarter', 'all'].includes(normalized)) {
|
|
2489
|
+
return normalized;
|
|
2490
|
+
}
|
|
2491
|
+
return 'week';
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
function getTaskBoardRangeLabel(taskBoardData) {
|
|
2495
|
+
const period = normalizeTaskBoardPeriod(taskBoardData?.period || state.taskBoardPeriod);
|
|
2496
|
+
if (period === 'all') return '全部';
|
|
2497
|
+
return `${safeText(taskBoardData?.start_date || '-')} ~ ${safeText(taskBoardData?.end_date || '-')}`;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
function renderTaskBoard() {
|
|
2501
|
+
const summary = document.getElementById('task-board-summary');
|
|
2502
|
+
const periodSelect = document.getElementById('task-board-period');
|
|
2503
|
+
const ownerInput = document.getElementById('task-board-owner-input');
|
|
2504
|
+
if (periodSelect instanceof HTMLSelectElement) {
|
|
2505
|
+
periodSelect.value = normalizeTaskBoardPeriod(state.taskBoardPeriod);
|
|
2506
|
+
}
|
|
2507
|
+
if (ownerInput instanceof HTMLInputElement) {
|
|
2508
|
+
ownerInput.value = state.taskBoardOwnerName || '';
|
|
2509
|
+
}
|
|
2510
|
+
if (!state.taskBoardData) {
|
|
2511
|
+
renderTaskBoardList('task-board-mine-list', []);
|
|
2512
|
+
renderTaskBoardList('task-board-others-list', []);
|
|
2513
|
+
if (summary instanceof HTMLElement) {
|
|
2514
|
+
summary.innerHTML = '';
|
|
2515
|
+
}
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
renderTaskBoardList('task-board-mine-list', state.taskBoardData.mine || []);
|
|
2519
|
+
renderTaskBoardList('task-board-others-list', state.taskBoardData.others || []);
|
|
2520
|
+
if (summary instanceof HTMLElement) {
|
|
2521
|
+
const counts = state.taskBoardData.counts || { mine: 0, others: 0, total: 0 };
|
|
2522
|
+
summary.innerHTML = `
|
|
2523
|
+
<div class="summary-card"><div class="label">统计区间</div><div class="value" style="font-size:13px;font-weight:500">${getTaskBoardRangeLabel(state.taskBoardData)}</div></div>
|
|
2524
|
+
<div class="summary-card"><div class="label">我的任务</div><div class="value">${Number(counts.mine || 0)}</div></div>
|
|
2525
|
+
<div class="summary-card"><div class="label">他人任务</div><div class="value">${Number(counts.others || 0)}</div></div>
|
|
2526
|
+
<div class="summary-card"><div class="label">总数</div><div class="value">${Number(counts.total || 0)}</div></div>
|
|
2527
|
+
`;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
async function loadTaskBoard() {
|
|
2532
|
+
const params = new URLSearchParams();
|
|
2533
|
+
params.set('period', normalizeTaskBoardPeriod(state.taskBoardPeriod));
|
|
2534
|
+
if (state.taskBoardOwnerName) {
|
|
2535
|
+
params.set('owner', state.taskBoardOwnerName);
|
|
2536
|
+
}
|
|
2537
|
+
state.taskBoardData = await apiRequest(`${API_BASE}/items/agenda?${params.toString()}`);
|
|
2538
|
+
renderTaskBoard();
|
|
2539
|
+
}
|
|
2540
|
+
|
|
1777
2541
|
async function loadSandbox(id) {
|
|
1778
2542
|
closeQuickChatPopover();
|
|
1779
2543
|
state.currentSandboxWritable = canWriteSandboxById(id);
|
|
@@ -1825,8 +2589,8 @@ function renderSandboxOverview() {
|
|
|
1825
2589
|
<div class="summary-card"><div class="label">待处理/进行中</div><div class="value">${byStatus.pending}/${byStatus.inProgress}</div></div>
|
|
1826
2590
|
<div class="summary-card"><div class="label">已完成</div><div class="value">${byStatus.done}</div></div>
|
|
1827
2591
|
<div class="summary-card"><div class="label">关联日记</div><div class="value">${diaries.length}</div></div>
|
|
1828
|
-
<div class="summary-card"><div class="label"
|
|
1829
|
-
<div class="summary-card"><div class="label"
|
|
2592
|
+
<div class="summary-card"><div class="label">问题(开放)</div><div class="value">${entityStats.issue?.total || 0}(${(entityStats.issue?.open || 0) + (entityStats.issue?.in_progress || 0) + (entityStats.issue?.blocked || 0)})</div></div>
|
|
2593
|
+
<div class="summary-card"><div class="label">知识 / 能力</div><div class="value">${entityStats.knowledge?.total || 0} / ${entityStats.capability?.total || 0}</div></div>
|
|
1830
2594
|
`;
|
|
1831
2595
|
}
|
|
1832
2596
|
|
|
@@ -2553,6 +3317,25 @@ function collectProjectSettingsPayload() {
|
|
|
2553
3317
|
};
|
|
2554
3318
|
}
|
|
2555
3319
|
|
|
3320
|
+
function collectIneffectiveProfileNames(payload) {
|
|
3321
|
+
const profiles = Array.isArray(payload?.profiles) ? payload.profiles : [];
|
|
3322
|
+
const bindings = Array.isArray(payload?.bindings) ? payload.bindings : [];
|
|
3323
|
+
const boundProfileIds = new Set();
|
|
3324
|
+
bindings.forEach((binding) => {
|
|
3325
|
+
const profileIds = Array.isArray(binding?.profile_ids) ? binding.profile_ids : [];
|
|
3326
|
+
profileIds.forEach((id) => boundProfileIds.add(String(id || '').trim()));
|
|
3327
|
+
});
|
|
3328
|
+
return profiles
|
|
3329
|
+
.filter((profile) => {
|
|
3330
|
+
const id = String(profile?.id || '').trim();
|
|
3331
|
+
const applyAll = Boolean(profile?.apply_to_all_users);
|
|
3332
|
+
if (!id || applyAll) return false;
|
|
3333
|
+
return !boundProfileIds.has(id);
|
|
3334
|
+
})
|
|
3335
|
+
.map((profile) => String(profile?.name || profile?.id || '').trim())
|
|
3336
|
+
.filter(Boolean);
|
|
3337
|
+
}
|
|
3338
|
+
|
|
2556
3339
|
function createSettingHeaderRow(key = '', value = '') {
|
|
2557
3340
|
const row = document.createElement('div');
|
|
2558
3341
|
row.className = 'settings-kv-row';
|
|
@@ -2606,6 +3389,20 @@ function showPage(pageId) {
|
|
|
2606
3389
|
const navPageId = pageId === 'sandbox-detail' ? 'sandboxes' : pageId;
|
|
2607
3390
|
const nav = document.querySelector(`[data-nav="${navPageId}"]`);
|
|
2608
3391
|
if (nav) nav.classList.add('active');
|
|
3392
|
+
if (['ai-engineering', 'aser-runtime', 'agent-club', 'teams', 'opencode-team-runner'].includes(navPageId)) {
|
|
3393
|
+
setAiEngineeringNavCollapsed(false);
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
function setAiEngineeringNavCollapsed(collapsed) {
|
|
3398
|
+
const navGroup = document.getElementById('ai-engineering-nav-group');
|
|
3399
|
+
const toggleBtn = document.getElementById('toggle-ai-engineering-nav');
|
|
3400
|
+
if (!(navGroup instanceof HTMLElement)) return;
|
|
3401
|
+
navGroup.classList.toggle('nav-group-collapsed', Boolean(collapsed));
|
|
3402
|
+
if (toggleBtn instanceof HTMLButtonElement) {
|
|
3403
|
+
toggleBtn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
3404
|
+
toggleBtn.textContent = collapsed ? '▸' : '▾';
|
|
3405
|
+
}
|
|
2609
3406
|
}
|
|
2610
3407
|
|
|
2611
3408
|
function editWorkItem(id) {
|
|
@@ -2629,8 +3426,18 @@ async function initApp() {
|
|
|
2629
3426
|
loadWorkItemAssigneePreference();
|
|
2630
3427
|
loadWorkTreeViewModePreference();
|
|
2631
3428
|
loadSandboxSortModePreference();
|
|
3429
|
+
loadTaskBoardOwnerNamePreference();
|
|
3430
|
+
loadAiEngineeringState();
|
|
2632
3431
|
renderQuickDiaryTargetLabel();
|
|
2633
3432
|
applyReadonlyMode();
|
|
3433
|
+
aserRuntimeView = createAserRuntimeView({
|
|
3434
|
+
readonly: state.readonly,
|
|
3435
|
+
hasAccess: state.fullAccess || state.pageAccess.sandboxes,
|
|
3436
|
+
getTeams: () => state.teams.map((team) => ({ ...team })),
|
|
3437
|
+
getProjectTeamAssignments: () => ({ ...(state.aserProjectTeamAssignments || {}) }),
|
|
3438
|
+
assignProjectTeam: (projectId, teamId) => assignAserProjectTeam(projectId, teamId),
|
|
3439
|
+
});
|
|
3440
|
+
aserRuntimeView.mount();
|
|
2634
3441
|
|
|
2635
3442
|
document.querySelectorAll('.nav-list a').forEach(link => {
|
|
2636
3443
|
link.addEventListener('click', (e) => {
|
|
@@ -2638,6 +3445,152 @@ async function initApp() {
|
|
|
2638
3445
|
window.location.hash = link.getAttribute('href').replace('#', '') || '/';
|
|
2639
3446
|
});
|
|
2640
3447
|
});
|
|
3448
|
+
|
|
3449
|
+
document.getElementById('agent-club-create-btn')?.addEventListener('click', () => {
|
|
3450
|
+
const nameInput = document.getElementById('agent-club-name');
|
|
3451
|
+
const roleInput = document.getElementById('agent-club-role');
|
|
3452
|
+
const knowledgeInput = document.getElementById('agent-club-knowledge');
|
|
3453
|
+
const skillsInput = document.getElementById('agent-club-skills');
|
|
3454
|
+
const taskTypesInput = document.getElementById('agent-club-task-types');
|
|
3455
|
+
const deliverablesInput = document.getElementById('agent-club-deliverables');
|
|
3456
|
+
const raciInput = document.getElementById('agent-club-raci-default');
|
|
3457
|
+
const qualityInput = document.getElementById('agent-club-quality-criteria');
|
|
3458
|
+
const riskInput = document.getElementById('agent-club-risk-limits');
|
|
3459
|
+
const name = String(nameInput?.value || '').trim();
|
|
3460
|
+
const role = String(roleInput?.value || '').trim();
|
|
3461
|
+
const knowledgeBackground = parseLineList(knowledgeInput?.value);
|
|
3462
|
+
const skills = parseSkillRows(skillsInput?.value);
|
|
3463
|
+
const taskTypes = parseLineList(taskTypesInput?.value);
|
|
3464
|
+
const deliverables = parseLineList(deliverablesInput?.value);
|
|
3465
|
+
const raciDefault = String(raciInput?.value || '').trim();
|
|
3466
|
+
const qualityCriteria = parseLineList(qualityInput?.value);
|
|
3467
|
+
const riskLimits = parseLineList(riskInput?.value);
|
|
3468
|
+
if (!name) {
|
|
3469
|
+
alert('请填写 Agent 名称');
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
const id = `agent-${Date.now()}`;
|
|
3473
|
+
state.agentClub.unshift(normalizeAgentRecord({
|
|
3474
|
+
id,
|
|
3475
|
+
name,
|
|
3476
|
+
role_type: role || 'generalist',
|
|
3477
|
+
knowledge_background: knowledgeBackground,
|
|
3478
|
+
skills,
|
|
3479
|
+
task_types: taskTypes,
|
|
3480
|
+
deliverables,
|
|
3481
|
+
raci_default: raciDefault,
|
|
3482
|
+
quality_criteria: qualityCriteria,
|
|
3483
|
+
risk_limits: riskLimits,
|
|
3484
|
+
}));
|
|
3485
|
+
persistAiEngineeringState();
|
|
3486
|
+
renderAgentClub();
|
|
3487
|
+
renderTeams();
|
|
3488
|
+
renderOpenCodeRunner();
|
|
3489
|
+
if (nameInput) nameInput.value = '';
|
|
3490
|
+
if (roleInput) roleInput.value = '';
|
|
3491
|
+
if (knowledgeInput) knowledgeInput.value = '';
|
|
3492
|
+
if (skillsInput) skillsInput.value = '';
|
|
3493
|
+
if (taskTypesInput) taskTypesInput.value = '';
|
|
3494
|
+
if (deliverablesInput) deliverablesInput.value = '';
|
|
3495
|
+
if (raciInput) raciInput.value = '';
|
|
3496
|
+
if (qualityInput) qualityInput.value = '';
|
|
3497
|
+
if (riskInput) riskInput.value = '';
|
|
3498
|
+
if (aserRuntimeView) aserRuntimeView.refreshTeamContext();
|
|
3499
|
+
});
|
|
3500
|
+
|
|
3501
|
+
document.getElementById('agent-club-list')?.addEventListener('click', (event) => {
|
|
3502
|
+
const target = event.target;
|
|
3503
|
+
if (!(target instanceof HTMLElement)) return;
|
|
3504
|
+
const deleteId = String(target.getAttribute('data-agent-delete-id') || '').trim();
|
|
3505
|
+
if (!deleteId) return;
|
|
3506
|
+
state.agentClub = state.agentClub.filter((agent) => String(agent.id) !== deleteId);
|
|
3507
|
+
state.teams = state.teams.map((team) => ({
|
|
3508
|
+
...team,
|
|
3509
|
+
agent_ids: team.agent_ids.filter((agentId) => String(agentId) !== deleteId),
|
|
3510
|
+
}));
|
|
3511
|
+
persistAiEngineeringState();
|
|
3512
|
+
renderAgentClub();
|
|
3513
|
+
renderTeams();
|
|
3514
|
+
renderOpenCodeRunner();
|
|
3515
|
+
if (aserRuntimeView) aserRuntimeView.refreshTeamContext();
|
|
3516
|
+
});
|
|
3517
|
+
|
|
3518
|
+
document.getElementById('teams-create-btn')?.addEventListener('click', () => {
|
|
3519
|
+
const nameInput = document.getElementById('teams-name');
|
|
3520
|
+
const agentSelect = document.getElementById('teams-agent-select');
|
|
3521
|
+
const name = String(nameInput?.value || '').trim();
|
|
3522
|
+
if (!name) {
|
|
3523
|
+
alert('请填写 Team 名称');
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3526
|
+
const agentIds = agentSelect instanceof HTMLSelectElement
|
|
3527
|
+
? Array.from(agentSelect.selectedOptions)
|
|
3528
|
+
.map((option) => String(option.value || '').trim())
|
|
3529
|
+
.filter(Boolean)
|
|
3530
|
+
.filter((agentId) => state.agentClub.some((agent) => String(agent.id) === agentId))
|
|
3531
|
+
: [];
|
|
3532
|
+
const id = `team-${Date.now()}`;
|
|
3533
|
+
state.teams.unshift(normalizeTeamRecord({ id, name, agent_ids: agentIds }));
|
|
3534
|
+
persistAiEngineeringState();
|
|
3535
|
+
renderTeams();
|
|
3536
|
+
renderOpenCodeRunner();
|
|
3537
|
+
if (nameInput) nameInput.value = '';
|
|
3538
|
+
if (agentSelect instanceof HTMLSelectElement) {
|
|
3539
|
+
Array.from(agentSelect.options).forEach((option) => {
|
|
3540
|
+
option.selected = false;
|
|
3541
|
+
});
|
|
3542
|
+
}
|
|
3543
|
+
if (aserRuntimeView) aserRuntimeView.refreshTeamContext();
|
|
3544
|
+
});
|
|
3545
|
+
|
|
3546
|
+
document.getElementById('teams-list')?.addEventListener('click', (event) => {
|
|
3547
|
+
const target = event.target;
|
|
3548
|
+
if (!(target instanceof HTMLElement)) return;
|
|
3549
|
+
const deleteId = String(target.getAttribute('data-team-delete-id') || '').trim();
|
|
3550
|
+
if (!deleteId) return;
|
|
3551
|
+
state.teams = state.teams.filter((team) => String(team.id) !== deleteId);
|
|
3552
|
+
Object.keys(state.aserProjectTeamAssignments || {}).forEach((projectId) => {
|
|
3553
|
+
if (String(state.aserProjectTeamAssignments[projectId]) === deleteId) {
|
|
3554
|
+
delete state.aserProjectTeamAssignments[projectId];
|
|
3555
|
+
}
|
|
3556
|
+
});
|
|
3557
|
+
persistAiEngineeringState();
|
|
3558
|
+
renderTeams();
|
|
3559
|
+
renderOpenCodeRunner();
|
|
3560
|
+
if (aserRuntimeView) aserRuntimeView.refreshTeamContext();
|
|
3561
|
+
});
|
|
3562
|
+
|
|
3563
|
+
document.getElementById('opencode-runner-project-select')?.addEventListener('change', (event) => {
|
|
3564
|
+
const target = event.target;
|
|
3565
|
+
if (!(target instanceof HTMLSelectElement)) return;
|
|
3566
|
+
state.opencodeRunnerSelectedProjectId = String(target.value || '').trim();
|
|
3567
|
+
const assigned = getAssignedTeamIdForProject(state.opencodeRunnerSelectedProjectId);
|
|
3568
|
+
if (assigned) state.opencodeRunnerSelectedTeamId = assigned;
|
|
3569
|
+
renderOpenCodeRunner();
|
|
3570
|
+
});
|
|
3571
|
+
|
|
3572
|
+
document.getElementById('opencode-runner-team-select')?.addEventListener('change', (event) => {
|
|
3573
|
+
const target = event.target;
|
|
3574
|
+
if (!(target instanceof HTMLSelectElement)) return;
|
|
3575
|
+
state.opencodeRunnerSelectedTeamId = String(target.value || '').trim();
|
|
3576
|
+
renderOpenCodeRunner();
|
|
3577
|
+
});
|
|
3578
|
+
|
|
3579
|
+
document.getElementById('opencode-runner-load-btn')?.addEventListener('click', () => {
|
|
3580
|
+
loadOpenCodeRunnerTasks().catch((error) => {
|
|
3581
|
+
alert(`获取任务失败: ${error?.message || error}`);
|
|
3582
|
+
});
|
|
3583
|
+
});
|
|
3584
|
+
|
|
3585
|
+
document.getElementById('opencode-runner-plan-btn')?.addEventListener('click', () => {
|
|
3586
|
+
planOpenCodeRunnerTasks();
|
|
3587
|
+
});
|
|
3588
|
+
|
|
3589
|
+
document.getElementById('opencode-runner-start-btn')?.addEventListener('click', () => {
|
|
3590
|
+
startOpenCodeRunner().catch((error) => {
|
|
3591
|
+
alert(`启动 Runner 失败: ${error?.message || error}`);
|
|
3592
|
+
});
|
|
3593
|
+
});
|
|
2641
3594
|
|
|
2642
3595
|
document.getElementById('add-sandbox-btn')?.addEventListener('click', () => {
|
|
2643
3596
|
if (state.readonly) return;
|
|
@@ -2772,13 +3725,49 @@ async function initApp() {
|
|
|
2772
3725
|
document.getElementById('item-dialog').close();
|
|
2773
3726
|
});
|
|
2774
3727
|
|
|
3728
|
+
document.getElementById('toggle-ai-engineering-nav')?.addEventListener('click', () => {
|
|
3729
|
+
const navGroup = document.getElementById('ai-engineering-nav-group');
|
|
3730
|
+
if (!(navGroup instanceof HTMLElement)) return;
|
|
3731
|
+
const collapsed = navGroup.classList.contains('nav-group-collapsed');
|
|
3732
|
+
setAiEngineeringNavCollapsed(!collapsed);
|
|
3733
|
+
});
|
|
3734
|
+
|
|
3735
|
+
document.getElementById('task-board-period')?.addEventListener('change', async (event) => {
|
|
3736
|
+
const value = String(event?.target?.value || 'week');
|
|
3737
|
+
state.taskBoardPeriod = normalizeTaskBoardPeriod(value);
|
|
3738
|
+
await loadTaskBoard();
|
|
3739
|
+
});
|
|
3740
|
+
|
|
3741
|
+
document.getElementById('task-board-owner-input')?.addEventListener('change', async (event) => {
|
|
3742
|
+
state.taskBoardOwnerName = String(event?.target?.value || '').trim();
|
|
3743
|
+
persistTaskBoardOwnerNamePreference();
|
|
3744
|
+
await loadTaskBoard();
|
|
3745
|
+
});
|
|
3746
|
+
|
|
3747
|
+
document.getElementById('task-board-refresh-btn')?.addEventListener('click', async () => {
|
|
3748
|
+
await loadTaskBoard();
|
|
3749
|
+
});
|
|
3750
|
+
|
|
2775
3751
|
document.getElementById('close-node-drawer-btn')?.addEventListener('click', () => {
|
|
2776
3752
|
closeNodeEntityDrawer();
|
|
2777
3753
|
});
|
|
2778
3754
|
|
|
2779
3755
|
document.getElementById('toggle-node-entity-form-btn')?.addEventListener('click', () => {
|
|
2780
3756
|
if (state.readonly) return;
|
|
2781
|
-
|
|
3757
|
+
const nextExpanded = !state.nodeEntityFormExpanded;
|
|
3758
|
+
if (nextExpanded) {
|
|
3759
|
+
const preferredType = ['todo', 'issue', 'knowledge', 'capability'].includes(state.nodeEntityFilter)
|
|
3760
|
+
? state.nodeEntityFilter
|
|
3761
|
+
: 'issue';
|
|
3762
|
+
if (!state.editingNodeEntityId) {
|
|
3763
|
+
const typeSelect = document.getElementById('entity-type-select');
|
|
3764
|
+
if (typeSelect instanceof HTMLSelectElement) {
|
|
3765
|
+
typeSelect.value = preferredType;
|
|
3766
|
+
applyEntityFormMode();
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
setNodeEntityFormExpanded(nextExpanded);
|
|
2782
3771
|
if (state.nodeEntityFormExpanded) {
|
|
2783
3772
|
document.getElementById('entity-title-input')?.focus();
|
|
2784
3773
|
}
|
|
@@ -2813,31 +3802,77 @@ async function initApp() {
|
|
|
2813
3802
|
const entity_type = document.getElementById('entity-type-select').value;
|
|
2814
3803
|
if (!title) return;
|
|
2815
3804
|
const rawStatus = document.getElementById('entity-status-select').value || '';
|
|
2816
|
-
const status = rawStatus || (entity_type === '
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
status,
|
|
2824
|
-
priority: document.getElementById('entity-priority-select').value || '',
|
|
2825
|
-
capability_type: document.getElementById('entity-capability-type-input').value || '',
|
|
2826
|
-
};
|
|
2827
|
-
|
|
3805
|
+
const status = rawStatus || (entity_type === 'todo'
|
|
3806
|
+
? 'pending'
|
|
3807
|
+
: entity_type === 'issue'
|
|
3808
|
+
? 'open'
|
|
3809
|
+
: entity_type === 'capability'
|
|
3810
|
+
? 'building'
|
|
3811
|
+
: '');
|
|
2828
3812
|
const isEditing = Boolean(state.editingNodeEntityId);
|
|
3813
|
+
const editingTodoId = isEditing && String(state.editingNodeEntityId).startsWith('todo:')
|
|
3814
|
+
? String(state.editingNodeEntityId).slice(5)
|
|
3815
|
+
: '';
|
|
3816
|
+
const editingEntityId = isEditing && !editingTodoId ? String(state.editingNodeEntityId) : '';
|
|
3817
|
+
|
|
2829
3818
|
setButtonState(btn, { disabled: true, text: isEditing ? '保存中...' : '添加中...' });
|
|
2830
3819
|
try {
|
|
2831
|
-
if (
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
3820
|
+
if (entity_type === 'todo') {
|
|
3821
|
+
const todoPayload = {
|
|
3822
|
+
name: title,
|
|
3823
|
+
parent_id: state.selectedNodeId,
|
|
3824
|
+
description: '',
|
|
3825
|
+
assignee: document.getElementById('entity-assignee-input').value || '',
|
|
3826
|
+
status,
|
|
3827
|
+
priority: document.getElementById('entity-priority-select').value || 'medium',
|
|
3828
|
+
extra_data: {
|
|
3829
|
+
todo: {
|
|
3830
|
+
is_todo: true,
|
|
3831
|
+
planned_start_date: document.getElementById('entity-todo-planned-start-input').value || '',
|
|
3832
|
+
due_date: document.getElementById('entity-todo-due-date-input').value || '',
|
|
3833
|
+
},
|
|
3834
|
+
},
|
|
3835
|
+
};
|
|
3836
|
+
const plannedStart = String(todoPayload.extra_data.todo.planned_start_date || '').trim();
|
|
3837
|
+
const dueDate = String(todoPayload.extra_data.todo.due_date || '').trim();
|
|
3838
|
+
if (plannedStart && dueDate && plannedStart > dueDate) {
|
|
3839
|
+
alert('计划启动日期不能晚于截止日期。');
|
|
3840
|
+
return;
|
|
3841
|
+
}
|
|
3842
|
+
if (editingTodoId) {
|
|
3843
|
+
await apiRequest(`${API_BASE}/items/${editingTodoId}`, {
|
|
3844
|
+
method: 'PUT',
|
|
3845
|
+
body: JSON.stringify(todoPayload),
|
|
3846
|
+
});
|
|
3847
|
+
} else {
|
|
3848
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/items`, {
|
|
3849
|
+
method: 'POST',
|
|
3850
|
+
body: JSON.stringify(todoPayload),
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
2836
3853
|
} else {
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
3854
|
+
const payload = {
|
|
3855
|
+
work_item_id: state.selectedNodeId,
|
|
3856
|
+
entity_type,
|
|
3857
|
+
title,
|
|
3858
|
+
content_md: document.getElementById('entity-content-input').value || '',
|
|
3859
|
+
assignee: document.getElementById('entity-assignee-input').value || '',
|
|
3860
|
+
due_date: document.getElementById('entity-due-date-input').value || '',
|
|
3861
|
+
status,
|
|
3862
|
+
priority: document.getElementById('entity-priority-select').value || '',
|
|
3863
|
+
capability_type: document.getElementById('entity-capability-type-input').value || '',
|
|
3864
|
+
};
|
|
3865
|
+
if (editingEntityId) {
|
|
3866
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/entities/${editingEntityId}`, {
|
|
3867
|
+
method: 'PUT',
|
|
3868
|
+
body: JSON.stringify(payload),
|
|
3869
|
+
});
|
|
3870
|
+
} else {
|
|
3871
|
+
await apiRequest(`${API_BASE}/sandboxes/${state.currentSandbox.id}/entities`, {
|
|
3872
|
+
method: 'POST',
|
|
3873
|
+
body: JSON.stringify(payload),
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
2841
3876
|
}
|
|
2842
3877
|
await loadSandbox(state.currentSandbox.id);
|
|
2843
3878
|
showNodeEntityDrawer(state.selectedNodeId);
|
|
@@ -2908,7 +3943,9 @@ async function initApp() {
|
|
|
2908
3943
|
if (state.readonly) return;
|
|
2909
3944
|
const dialog = document.getElementById('item-dialog');
|
|
2910
3945
|
const editId = dialog.dataset.editId || null;
|
|
2911
|
-
const
|
|
3946
|
+
const editingItem = editId
|
|
3947
|
+
? state.currentSandbox?.items?.find((item) => String(item.id) === String(editId))
|
|
3948
|
+
: null;
|
|
2912
3949
|
|
|
2913
3950
|
const data = {
|
|
2914
3951
|
name: document.getElementById('new-item-name').value,
|
|
@@ -2918,6 +3955,10 @@ async function initApp() {
|
|
|
2918
3955
|
priority: document.getElementById('new-item-priority').value,
|
|
2919
3956
|
parent_id: document.getElementById('new-item-parent').value || null,
|
|
2920
3957
|
};
|
|
3958
|
+
const mergedExtraData = {
|
|
3959
|
+
...(editingItem?.extra_data || {}),
|
|
3960
|
+
};
|
|
3961
|
+
data.extra_data = mergedExtraData;
|
|
2921
3962
|
|
|
2922
3963
|
if (!data.name) {
|
|
2923
3964
|
return;
|
|
@@ -3387,6 +4428,15 @@ async function initApp() {
|
|
|
3387
4428
|
if (state.readonly || (!state.fullAccess && !state.pageAccess.settings)) return;
|
|
3388
4429
|
try {
|
|
3389
4430
|
const payload = collectProjectSettingsPayload();
|
|
4431
|
+
const ineffectiveProfiles = collectIneffectiveProfileNames(payload);
|
|
4432
|
+
if (ineffectiveProfiles.length) {
|
|
4433
|
+
const preview = ineffectiveProfiles.slice(0, 3).join('、');
|
|
4434
|
+
const more = ineffectiveProfiles.length > 3 ? ` 等 ${ineffectiveProfiles.length} 个` : '';
|
|
4435
|
+
const proceed = confirm(
|
|
4436
|
+
`检测到未生效的 Profile:${preview}${more}。\n这些 Profile 既未勾选 apply_to_all_users,也未被任何 Binding 绑定。\n是否仍然继续保存?`,
|
|
4437
|
+
);
|
|
4438
|
+
if (!proceed) return;
|
|
4439
|
+
}
|
|
3390
4440
|
await apiRequest(`${API_BASE}/project-settings`, {
|
|
3391
4441
|
method: 'PUT',
|
|
3392
4442
|
body: JSON.stringify(payload),
|
|
@@ -3474,6 +4524,60 @@ async function initApp() {
|
|
|
3474
4524
|
alert('汇报已复制');
|
|
3475
4525
|
});
|
|
3476
4526
|
|
|
4527
|
+
const parseDownloadFilename = (contentDisposition, fallbackName) => {
|
|
4528
|
+
const encodedMatch = String(contentDisposition || '').match(/filename\*=UTF-8''([^;]+)/i);
|
|
4529
|
+
if (encodedMatch?.[1]) {
|
|
4530
|
+
try {
|
|
4531
|
+
return decodeURIComponent(encodedMatch[1]);
|
|
4532
|
+
} catch {
|
|
4533
|
+
// Ignore malformed encoding and fallback.
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
const plainMatch = String(contentDisposition || '').match(/filename="([^"]+)"/i);
|
|
4537
|
+
if (plainMatch?.[1]) return plainMatch[1];
|
|
4538
|
+
return fallbackName;
|
|
4539
|
+
};
|
|
4540
|
+
|
|
4541
|
+
document.getElementById('export-sandbox-markdown-btn')?.addEventListener('click', async () => {
|
|
4542
|
+
if (!state.currentSandbox) return;
|
|
4543
|
+
const response = await fetch(`${API_BASE}/sandboxes/${encodeURIComponent(state.currentSandbox.id)}/export-markdown`);
|
|
4544
|
+
if (!response.ok) {
|
|
4545
|
+
alert('导出失败');
|
|
4546
|
+
return;
|
|
4547
|
+
}
|
|
4548
|
+
const blob = await response.blob();
|
|
4549
|
+
const contentDisposition = response.headers.get('content-disposition') || '';
|
|
4550
|
+
const filename = parseDownloadFilename(contentDisposition, `${state.currentSandbox.name || 'sandbox'}.md`);
|
|
4551
|
+
const href = URL.createObjectURL(blob);
|
|
4552
|
+
const link = document.createElement('a');
|
|
4553
|
+
link.href = href;
|
|
4554
|
+
link.download = filename;
|
|
4555
|
+
document.body.appendChild(link);
|
|
4556
|
+
link.click();
|
|
4557
|
+
link.remove();
|
|
4558
|
+
URL.revokeObjectURL(href);
|
|
4559
|
+
});
|
|
4560
|
+
|
|
4561
|
+
document.getElementById('export-sandbox-excel-btn')?.addEventListener('click', async () => {
|
|
4562
|
+
if (!state.currentSandbox) return;
|
|
4563
|
+
const response = await fetch(`${API_BASE}/sandboxes/${encodeURIComponent(state.currentSandbox.id)}/export-excel`);
|
|
4564
|
+
if (!response.ok) {
|
|
4565
|
+
alert('导出失败');
|
|
4566
|
+
return;
|
|
4567
|
+
}
|
|
4568
|
+
const blob = await response.blob();
|
|
4569
|
+
const contentDisposition = response.headers.get('content-disposition') || '';
|
|
4570
|
+
const filename = parseDownloadFilename(contentDisposition, `${state.currentSandbox.name || 'sandbox'}.xlsx`);
|
|
4571
|
+
const href = URL.createObjectURL(blob);
|
|
4572
|
+
const link = document.createElement('a');
|
|
4573
|
+
link.href = href;
|
|
4574
|
+
link.download = filename;
|
|
4575
|
+
document.body.appendChild(link);
|
|
4576
|
+
link.click();
|
|
4577
|
+
link.remove();
|
|
4578
|
+
URL.revokeObjectURL(href);
|
|
4579
|
+
});
|
|
4580
|
+
|
|
3477
4581
|
document.getElementById('generate-insight-btn')?.addEventListener('click', async () => {
|
|
3478
4582
|
if (!state.currentSandbox) return;
|
|
3479
4583
|
const output = document.getElementById('sandbox-insight-output');
|
|
@@ -3574,6 +4678,12 @@ async function initApp() {
|
|
|
3574
4678
|
} else if (state.canAccessSystemSettings) {
|
|
3575
4679
|
window.location.hash = '/system-settings';
|
|
3576
4680
|
}
|
|
4681
|
+
} else if (pathHash === '/ai-engineering') {
|
|
4682
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
4683
|
+
window.location.hash = '/';
|
|
4684
|
+
return;
|
|
4685
|
+
}
|
|
4686
|
+
showPage('ai-engineering');
|
|
3577
4687
|
} else if (pathHash === '/sandboxes') {
|
|
3578
4688
|
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
3579
4689
|
window.location.hash = '/';
|
|
@@ -3603,6 +4713,44 @@ async function initApp() {
|
|
|
3603
4713
|
showPage('diaries');
|
|
3604
4714
|
await loadDiaries();
|
|
3605
4715
|
await loadSandboxes();
|
|
4716
|
+
} else if (pathHash === '/tasks') {
|
|
4717
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
4718
|
+
window.location.hash = '/';
|
|
4719
|
+
return;
|
|
4720
|
+
}
|
|
4721
|
+
showPage('tasks');
|
|
4722
|
+
await loadTaskBoard();
|
|
4723
|
+
} else if (pathHash === '/aser-runtime') {
|
|
4724
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
4725
|
+
window.location.hash = '/';
|
|
4726
|
+
return;
|
|
4727
|
+
}
|
|
4728
|
+
showPage('aser-runtime');
|
|
4729
|
+
if (aserRuntimeView) {
|
|
4730
|
+
await aserRuntimeView.loadProjects();
|
|
4731
|
+
}
|
|
4732
|
+
} else if (pathHash === '/agent-club') {
|
|
4733
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
4734
|
+
window.location.hash = '/';
|
|
4735
|
+
return;
|
|
4736
|
+
}
|
|
4737
|
+
showPage('agent-club');
|
|
4738
|
+
renderAgentClub();
|
|
4739
|
+
} else if (pathHash === '/teams') {
|
|
4740
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
4741
|
+
window.location.hash = '/';
|
|
4742
|
+
return;
|
|
4743
|
+
}
|
|
4744
|
+
showPage('teams');
|
|
4745
|
+
renderTeams();
|
|
4746
|
+
} else if (pathHash === '/opencode-team-runner') {
|
|
4747
|
+
if (!state.pageAccess.sandboxes && !state.fullAccess) {
|
|
4748
|
+
window.location.hash = '/';
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
showPage('opencode-team-runner');
|
|
4752
|
+
await loadOpenCodeRunnerProjects();
|
|
4753
|
+
renderOpenCodeRunner();
|
|
3606
4754
|
} else if (pathHash === '/changes') {
|
|
3607
4755
|
if (!state.pageAccess.changes && !state.fullAccess) {
|
|
3608
4756
|
window.location.hash = '/';
|