@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
|
@@ -0,0 +1,2152 @@
|
|
|
1
|
+
import { API_BASE, apiRequest, safeText } from './shared.js';
|
|
2
|
+
|
|
3
|
+
function byId(id) {
|
|
4
|
+
return document.getElementById(id);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function formatDateTime(value) {
|
|
8
|
+
if (!value) return '-';
|
|
9
|
+
const date = new Date(value);
|
|
10
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
11
|
+
return date.toLocaleString();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseOptionalJsonText(rawValue) {
|
|
15
|
+
const raw = String(rawValue || '').trim();
|
|
16
|
+
if (!raw) return {};
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(raw);
|
|
19
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
|
20
|
+
return parsed;
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error('payload JSON 格式不正确');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PM_STAGE_ORDER = [
|
|
27
|
+
'create_requirement',
|
|
28
|
+
'design_system',
|
|
29
|
+
'clarify_design',
|
|
30
|
+
'implement_feature',
|
|
31
|
+
'review_code',
|
|
32
|
+
'report_issue',
|
|
33
|
+
'escalate_issue',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const PM_STAGE_LABELS = {
|
|
37
|
+
create_requirement: '需求定义',
|
|
38
|
+
design_system: '架构设计',
|
|
39
|
+
clarify_design: '澄清回路',
|
|
40
|
+
implement_feature: '开发实现',
|
|
41
|
+
review_code: '评审验收',
|
|
42
|
+
report_issue: '问题记录',
|
|
43
|
+
escalate_issue: '管理上升',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const DEFAULT_PROTOCOL_PRESET = {
|
|
47
|
+
id: 'engineering_protocol_v1',
|
|
48
|
+
version: 1,
|
|
49
|
+
roles: [
|
|
50
|
+
{ role_id: 'product_manager', intents: ['create_requirement', 'escalate_issue'] },
|
|
51
|
+
{ role_id: 'architect', intents: ['design_system', 'clarify_design'] },
|
|
52
|
+
{ role_id: 'developer', intents: ['implement_feature', 'report_issue', 'clarify_design'] },
|
|
53
|
+
{ role_id: 'reviewer', intents: ['review_code', 'escalate_issue'] },
|
|
54
|
+
],
|
|
55
|
+
intents: PM_STAGE_ORDER,
|
|
56
|
+
intent_rules: [
|
|
57
|
+
{ intent: 'design_system', requires: ['create_requirement'] },
|
|
58
|
+
{ intent: 'clarify_design', requires: ['design_system'] },
|
|
59
|
+
{ intent: 'implement_feature', requires: ['design_system', 'clarify_design'] },
|
|
60
|
+
{ intent: 'review_code', requires: ['implement_feature'] },
|
|
61
|
+
{ intent: 'report_issue', requires: ['implement_feature'] },
|
|
62
|
+
{ intent: 'escalate_issue', requires: ['report_issue'] },
|
|
63
|
+
],
|
|
64
|
+
evaluation_templates: [],
|
|
65
|
+
quality_gates: [
|
|
66
|
+
{ gate_id: 'G1', stage: 'create_requirement', required_evidence: ['requirement_doc'], pass_when: 'requirement_is_complete' },
|
|
67
|
+
{ gate_id: 'G2', stage: 'design_system', required_evidence: ['design_doc'], pass_when: 'design_review_passed' },
|
|
68
|
+
{ gate_id: 'G3', stage: 'implement_feature', required_evidence: ['pr', 'test_report'], pass_when: 'implementation_tests_passed' },
|
|
69
|
+
{ gate_id: 'G4', stage: 'review_code', required_evidence: ['review_record'], pass_when: 'review_accepted' },
|
|
70
|
+
{ gate_id: 'G5', stage: 'escalate_issue', required_evidence: ['risk_summary'], pass_when: 'escalation_acknowledged' },
|
|
71
|
+
],
|
|
72
|
+
loop_actions: [
|
|
73
|
+
{ id: 'L1', taxonomy: 'clarify_design', trigger_event_types: ['message_posted'], required_links: ['parent'], closure_evidence: ['clarification_note'] },
|
|
74
|
+
{ id: 'L2', taxonomy: 'report_issue', trigger_event_types: ['issue_reported', 'task_blocked'], required_links: ['parent'], closure_evidence: ['issue_record'] },
|
|
75
|
+
{ id: 'L3', taxonomy: 'escalate_issue', trigger_event_types: ['escalation_requested'], required_links: ['parent', 'related'], closure_evidence: ['risk_summary'] },
|
|
76
|
+
],
|
|
77
|
+
updated_at: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const ROLE_STYLE = {
|
|
81
|
+
product_manager: { label: 'PM', tone: 'pm' },
|
|
82
|
+
architect: { label: 'ARCH', tone: 'arch' },
|
|
83
|
+
developer: { label: 'DEV', tone: 'dev' },
|
|
84
|
+
reviewer: { label: 'REV', tone: 'rev' },
|
|
85
|
+
};
|
|
86
|
+
const ROLE_ORDER = ['product_manager', 'architect', 'developer', 'reviewer', 'system'];
|
|
87
|
+
|
|
88
|
+
function taskStatusClass(status) {
|
|
89
|
+
const normalized = String(status || 'created');
|
|
90
|
+
if (normalized === 'accepted' || normalized === 'completed') return 'is-done';
|
|
91
|
+
if (normalized === 'blocked' || normalized === 'failed') return 'is-risk';
|
|
92
|
+
if (normalized === 'evaluating') return 'is-review';
|
|
93
|
+
if (normalized === 'in_progress') return 'is-progress';
|
|
94
|
+
return 'is-created';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isReplayEligibleStatus(status) {
|
|
98
|
+
const normalized = String(status || '');
|
|
99
|
+
return normalized === 'accepted' || normalized === 'completed';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function protocolCoverageStatus(stats) {
|
|
103
|
+
const safe = stats || { total: 0, done: 0, active: 0, blocked: 0 };
|
|
104
|
+
if (safe.blocked > 0) return { key: 'risk', label: '阻塞' };
|
|
105
|
+
if (safe.active > 0) return { key: 'active', label: '进行中' };
|
|
106
|
+
if (safe.done > 0) return { key: 'done', label: '已完成' };
|
|
107
|
+
return { key: 'idle', label: '未触发' };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function clampNumber(value, min, max) {
|
|
111
|
+
return Math.min(max, Math.max(min, value));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isLoopTaxonomy(taxonomy) {
|
|
115
|
+
return ['clarify_design', 'report_issue', 'escalate_issue'].includes(String(taxonomy || ''));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function classifySystemInput(rawText) {
|
|
119
|
+
const text = String(rawText || '').toLowerCase();
|
|
120
|
+
if (!text) return null;
|
|
121
|
+
if (text.includes('上升') || text.includes('escalat') || text.includes('风险')) {
|
|
122
|
+
return { type: 'escalation_requested', role: 'reviewer', actor: 'Reviewer Carol', actorKind: 'human', taxonomy: 'escalate_issue' };
|
|
123
|
+
}
|
|
124
|
+
if (text.includes('问题') || text.includes('阻塞') || text.includes('issue') || text.includes('block')) {
|
|
125
|
+
return { type: 'issue_reported', role: 'developer', actor: 'DevAgent', actorKind: 'agent', taxonomy: 'report_issue' };
|
|
126
|
+
}
|
|
127
|
+
if (text.includes('评审') || text.includes('review')) {
|
|
128
|
+
return { type: 'review_requested', role: 'reviewer', actor: 'Reviewer Carol', actorKind: 'human', taxonomy: 'review_code' };
|
|
129
|
+
}
|
|
130
|
+
if (text.includes('实现') || text.includes('开发') || text.includes('implement')) {
|
|
131
|
+
return { type: 'implementation_reported', role: 'developer', actor: 'DevAgent', actorKind: 'agent', taxonomy: 'implement_feature' };
|
|
132
|
+
}
|
|
133
|
+
if (text.includes('设计') || text.includes('架构') || text.includes('design')) {
|
|
134
|
+
return { type: 'design_defined', role: 'architect', actor: 'Architect Bob', actorKind: 'human', taxonomy: 'design_system' };
|
|
135
|
+
}
|
|
136
|
+
if (text.includes('需求') || text.includes('requirement')) {
|
|
137
|
+
return { type: 'requirement_defined', role: 'product_manager', actor: 'PM Alice', actorKind: 'human', taxonomy: 'create_requirement' };
|
|
138
|
+
}
|
|
139
|
+
return { type: 'message_posted', role: 'developer', actor: 'DevAgent', actorKind: 'agent', taxonomy: 'clarify_design' };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createAserRuntimeView(options = {}) {
|
|
143
|
+
const state = {
|
|
144
|
+
readonly: Boolean(options.readonly),
|
|
145
|
+
hasAccess: Boolean(options.hasAccess),
|
|
146
|
+
projects: [],
|
|
147
|
+
selectedProjectId: '',
|
|
148
|
+
selectedProject: null,
|
|
149
|
+
selectedProjectState: null,
|
|
150
|
+
selectedProtocol: null,
|
|
151
|
+
selectedProtocolPresetId: DEFAULT_PROTOCOL_PRESET.id,
|
|
152
|
+
protocolViewMode: 'graph',
|
|
153
|
+
protocolCoverageEnabled: true,
|
|
154
|
+
protocolPresets: [DEFAULT_PROTOCOL_PRESET],
|
|
155
|
+
projectListPage: 1,
|
|
156
|
+
projectListPageSize: 6,
|
|
157
|
+
viewMode: 'chat',
|
|
158
|
+
threadFilter: 'all',
|
|
159
|
+
focusedEntity: null,
|
|
160
|
+
selectedRunTeamId: '',
|
|
161
|
+
demo: {
|
|
162
|
+
started: false,
|
|
163
|
+
running: false,
|
|
164
|
+
auto: false,
|
|
165
|
+
stepIndex: -1,
|
|
166
|
+
projectId: '',
|
|
167
|
+
context: {},
|
|
168
|
+
logs: [],
|
|
169
|
+
steps: [],
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
function sleep(ms) {
|
|
174
|
+
return new Promise((resolve) => {
|
|
175
|
+
window.setTimeout(resolve, ms);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getTasks() {
|
|
180
|
+
const tasks = Array.isArray(state.selectedProject?.tasks) ? [...state.selectedProject.tasks] : [];
|
|
181
|
+
return tasks.sort((a, b) => String(a.created_at || '').localeCompare(String(b.created_at || '')));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function getActiveProtocol() {
|
|
185
|
+
if (state.selectedProtocol && state.selectedProtocol.id === state.selectedProtocolPresetId) {
|
|
186
|
+
return state.selectedProtocol;
|
|
187
|
+
}
|
|
188
|
+
const fromPreset = state.protocolPresets.find((preset) => String(preset.id) === String(state.selectedProtocolPresetId));
|
|
189
|
+
return fromPreset || state.selectedProtocol || DEFAULT_PROTOCOL_PRESET;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function findTaskById(taskId) {
|
|
193
|
+
return getTasks().find((task) => String(task.id) === String(taskId)) || null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getFocusedTask() {
|
|
197
|
+
if (!state.focusedEntity?.taskId) return null;
|
|
198
|
+
return findTaskById(state.focusedEntity.taskId);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function setFocusedTask(taskId, source) {
|
|
202
|
+
const task = findTaskById(taskId);
|
|
203
|
+
if (!task) return;
|
|
204
|
+
state.focusedEntity = { type: 'task', source, taskId: task.id };
|
|
205
|
+
renderAll();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function openDialog(id) {
|
|
209
|
+
const dialog = byId(id);
|
|
210
|
+
if (dialog instanceof HTMLDialogElement) dialog.showModal();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function closeDialog(id) {
|
|
214
|
+
const dialog = byId(id);
|
|
215
|
+
if (dialog instanceof HTMLDialogElement) dialog.close();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function setAccessContext(input = {}) {
|
|
219
|
+
if (typeof input.readonly === 'boolean') state.readonly = input.readonly;
|
|
220
|
+
if (typeof input.hasAccess === 'boolean') state.hasAccess = input.hasAccess;
|
|
221
|
+
renderAll();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getTeamList() {
|
|
225
|
+
const input = typeof options.getTeams === 'function' ? options.getTeams() : [];
|
|
226
|
+
return Array.isArray(input) ? input : [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function getProjectTeamAssignments() {
|
|
230
|
+
const input = typeof options.getProjectTeamAssignments === 'function' ? options.getProjectTeamAssignments() : {};
|
|
231
|
+
if (!input || typeof input !== 'object') return {};
|
|
232
|
+
return input;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getAssignedTeamId(projectId) {
|
|
236
|
+
const pid = String(projectId || '').trim();
|
|
237
|
+
if (!pid) return '';
|
|
238
|
+
const assignmentMap = getProjectTeamAssignments();
|
|
239
|
+
return String(assignmentMap[pid] || '').trim();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getTeamById(teamId) {
|
|
243
|
+
const id = String(teamId || '').trim();
|
|
244
|
+
if (!id) return null;
|
|
245
|
+
return getTeamList().find((team) => String(team?.id || '') === id) || null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function refreshSelectedRunTeam() {
|
|
249
|
+
const teams = getTeamList();
|
|
250
|
+
const assignedTeamId = getAssignedTeamId(state.selectedProjectId);
|
|
251
|
+
if (assignedTeamId && teams.some((team) => String(team.id) === assignedTeamId)) {
|
|
252
|
+
state.selectedRunTeamId = assignedTeamId;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (state.selectedRunTeamId && teams.some((team) => String(team.id) === String(state.selectedRunTeamId))) return;
|
|
256
|
+
state.selectedRunTeamId = teams[0] ? String(teams[0].id || '') : '';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function renderRunTeamSelector() {
|
|
260
|
+
const select = byId('aser-run-team-select');
|
|
261
|
+
if (!(select instanceof HTMLSelectElement)) return;
|
|
262
|
+
const teams = getTeamList();
|
|
263
|
+
refreshSelectedRunTeam();
|
|
264
|
+
if (!teams.length) {
|
|
265
|
+
select.innerHTML = '<option value="">暂无 Team(先去 Agent Club/Teams 定义)</option>';
|
|
266
|
+
select.value = '';
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
select.innerHTML = teams
|
|
270
|
+
.map((team) => `<option value="${safeText(team.id)}">${safeText(team.name || team.id)}</option>`)
|
|
271
|
+
.join('');
|
|
272
|
+
select.value = String(state.selectedRunTeamId || teams[0].id || '');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function renderControls() {
|
|
276
|
+
const writeDisabled = state.readonly || !state.hasAccess;
|
|
277
|
+
const projectDisabled = writeDisabled || !state.selectedProjectId;
|
|
278
|
+
const teams = getTeamList();
|
|
279
|
+
const hasTeamOptions = teams.length > 0;
|
|
280
|
+
refreshSelectedRunTeam();
|
|
281
|
+
const runTeamMissing = !String(state.selectedRunTeamId || '').trim();
|
|
282
|
+
const focusedTask = getFocusedTask();
|
|
283
|
+
const replayEligible = Boolean(focusedTask && isReplayEligibleStatus(focusedTask.status));
|
|
284
|
+
renderRunTeamSelector();
|
|
285
|
+
const controlMap = {
|
|
286
|
+
'aser-open-create-project-dialog-btn': writeDisabled,
|
|
287
|
+
'aser-load-complex-scenario-btn': writeDisabled,
|
|
288
|
+
'aser-open-protocol-dialog-btn': projectDisabled,
|
|
289
|
+
'aser-submit-event-btn': projectDisabled,
|
|
290
|
+
'aser-run-team-select': writeDisabled || !hasTeamOptions,
|
|
291
|
+
'aser-assign-team-btn': projectDisabled || !hasTeamOptions || runTeamMissing,
|
|
292
|
+
'aser-start-run-btn': projectDisabled || !hasTeamOptions || runTeamMissing,
|
|
293
|
+
'aser-thread-filter-select': projectDisabled,
|
|
294
|
+
'aser-protocol-preset-select': writeDisabled,
|
|
295
|
+
'aser-create-project-protocol-select': writeDisabled,
|
|
296
|
+
'aser-create-project-btn': writeDisabled,
|
|
297
|
+
'aser-load-protocol-btn': projectDisabled,
|
|
298
|
+
'aser-save-protocol-btn': projectDisabled,
|
|
299
|
+
'aser-demo-start-btn': writeDisabled || !replayEligible,
|
|
300
|
+
'aser-demo-next-btn': writeDisabled || !replayEligible || !state.demo.started || state.demo.running,
|
|
301
|
+
'aser-demo-auto-btn': writeDisabled || !replayEligible || !state.demo.started || state.demo.running,
|
|
302
|
+
'aser-demo-stop-btn': writeDisabled || !state.demo.auto,
|
|
303
|
+
'aser-demo-reset-btn': writeDisabled || !replayEligible || state.demo.running,
|
|
304
|
+
};
|
|
305
|
+
Object.entries(controlMap).forEach(([id, disabled]) => {
|
|
306
|
+
const el = byId(id);
|
|
307
|
+
if (el instanceof HTMLButtonElement || el instanceof HTMLSelectElement) el.disabled = disabled;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function renderSummary() {
|
|
312
|
+
const total = byId('aser-summary-total-projects');
|
|
313
|
+
const updated = byId('aser-summary-last-updated');
|
|
314
|
+
if (total instanceof HTMLElement) total.textContent = String(state.projects.length);
|
|
315
|
+
if (updated instanceof HTMLElement) {
|
|
316
|
+
const latest = [...state.projects]
|
|
317
|
+
.sort((a, b) => String(b.updated_at || '').localeCompare(String(a.updated_at || '')))[0];
|
|
318
|
+
updated.textContent = latest ? formatDateTime(latest.updated_at) : '-';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function renderProtocolPresetSelectors() {
|
|
323
|
+
const topSelect = byId('aser-protocol-preset-select');
|
|
324
|
+
const createSelect = byId('aser-create-project-protocol-select');
|
|
325
|
+
const options = state.protocolPresets.map((preset) => (
|
|
326
|
+
`<option value="${safeText(preset.id)}">${safeText(preset.id)} (v${safeText(String(preset.version || 1))})</option>`
|
|
327
|
+
)).join('');
|
|
328
|
+
if (topSelect instanceof HTMLSelectElement) {
|
|
329
|
+
topSelect.innerHTML = options;
|
|
330
|
+
topSelect.value = state.selectedProtocolPresetId || '';
|
|
331
|
+
}
|
|
332
|
+
if (createSelect instanceof HTMLSelectElement) {
|
|
333
|
+
createSelect.innerHTML = options;
|
|
334
|
+
createSelect.value = state.selectedProtocolPresetId || '';
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function renderDemoPanel() {
|
|
339
|
+
const titleEl = byId('aser-replay-title');
|
|
340
|
+
const actionWrap = byId('aser-replay-actions');
|
|
341
|
+
const progress = byId('aser-demo-progress');
|
|
342
|
+
const progressInner = byId('aser-demo-progress-inner');
|
|
343
|
+
const stepsEl = byId('aser-demo-steps');
|
|
344
|
+
const logsEl = byId('aser-demo-logs');
|
|
345
|
+
const focusedTask = getFocusedTask();
|
|
346
|
+
const replayEligible = Boolean(focusedTask && isReplayEligibleStatus(focusedTask.status));
|
|
347
|
+
const tasks = getTasks();
|
|
348
|
+
const events = Array.isArray(state.selectedProject?.events) ? [...state.selectedProject.events] : [];
|
|
349
|
+
const latestUserInputEvent = events
|
|
350
|
+
.filter((row) => String(row?.payload?.source || '') === 'system_input')
|
|
351
|
+
.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')))[0] || null;
|
|
352
|
+
const projectState = state.selectedProjectState || null;
|
|
353
|
+
const totalTasks = Number(projectState?.total_tasks || tasks.length || 0);
|
|
354
|
+
const acceptedTasks = Number(projectState?.accepted_tasks || 0);
|
|
355
|
+
const blockedTasks = Number(projectState?.blocked_tasks || 0);
|
|
356
|
+
const inProgressTasks = Math.max(0, totalTasks - acceptedTasks - blockedTasks);
|
|
357
|
+
const completion = Number(projectState?.feature_completion || 0);
|
|
358
|
+
const latestInputText = latestUserInputEvent
|
|
359
|
+
? String(
|
|
360
|
+
latestUserInputEvent.payload?.user_input
|
|
361
|
+
|| latestUserInputEvent.payload?.raw_input
|
|
362
|
+
|| '-',
|
|
363
|
+
)
|
|
364
|
+
: '-';
|
|
365
|
+
const latestInputMeta = latestUserInputEvent
|
|
366
|
+
? `${formatDateTime(latestUserInputEvent.created_at)}`
|
|
367
|
+
: '暂无用户输入记录';
|
|
368
|
+
|
|
369
|
+
if (!replayEligible && state.demo.started) {
|
|
370
|
+
state.demo.started = false;
|
|
371
|
+
state.demo.auto = false;
|
|
372
|
+
state.demo.running = false;
|
|
373
|
+
state.demo.stepIndex = -1;
|
|
374
|
+
state.demo.steps = [];
|
|
375
|
+
}
|
|
376
|
+
if (titleEl instanceof HTMLElement) {
|
|
377
|
+
titleEl.textContent = replayEligible
|
|
378
|
+
? `任务重访(${focusedTask.title || focusedTask.id})`
|
|
379
|
+
: '最新状态全景';
|
|
380
|
+
}
|
|
381
|
+
if (actionWrap instanceof HTMLElement) {
|
|
382
|
+
actionWrap.classList.toggle('hidden', !replayEligible);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const totalSteps = state.demo.steps.length || 0;
|
|
386
|
+
const doneSteps = Math.max(0, Math.min(totalSteps, state.demo.stepIndex + 1));
|
|
387
|
+
if (progress instanceof HTMLElement) {
|
|
388
|
+
if (!replayEligible) {
|
|
389
|
+
progress.textContent = '当前任务未完成,展示最新状态全景。';
|
|
390
|
+
} else if (!state.demo.started) {
|
|
391
|
+
progress.textContent = '未开始重放';
|
|
392
|
+
} else if (state.demo.stepIndex >= state.demo.steps.length - 1) {
|
|
393
|
+
progress.textContent = `重放完成(${totalSteps}/${totalSteps})`;
|
|
394
|
+
} else {
|
|
395
|
+
progress.textContent = `重放中:第 ${doneSteps} / ${totalSteps} 步${state.demo.auto ? '(自动播放)' : ''}`;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (progressInner instanceof HTMLElement) {
|
|
399
|
+
const ratio = totalSteps <= 0 ? 0 : (doneSteps / totalSteps) * 100;
|
|
400
|
+
progressInner.style.width = `${Math.max(0, Math.min(100, ratio))}%`;
|
|
401
|
+
}
|
|
402
|
+
if (stepsEl instanceof HTMLElement) {
|
|
403
|
+
if (!replayEligible) {
|
|
404
|
+
stepsEl.innerHTML = `
|
|
405
|
+
<div class="aser-demo-step done">任务输入:${safeText(latestInputText)}</div>
|
|
406
|
+
<div class="aser-demo-step">输入时间:${safeText(latestInputMeta)}</div>
|
|
407
|
+
<div class="aser-demo-step">执行情况:总任务 ${safeText(String(totalTasks))} | 完成 ${safeText(String(acceptedTasks))} | 进行 ${safeText(String(inProgressTasks))} | 阻塞 ${safeText(String(blockedTasks))}</div>
|
|
408
|
+
<div class="aser-demo-step">完成度:${safeText(String(completion))}%</div>
|
|
409
|
+
`;
|
|
410
|
+
} else if (!state.demo.started || !state.demo.steps.length) {
|
|
411
|
+
stepsEl.innerHTML = '<div class="aser-demo-empty">该任务已完成,可点击“开始重放”。</div>';
|
|
412
|
+
} else {
|
|
413
|
+
stepsEl.innerHTML = state.demo.steps.map((step, index) => {
|
|
414
|
+
const done = index <= state.demo.stepIndex;
|
|
415
|
+
const active = index === state.demo.stepIndex + 1 && state.demo.running;
|
|
416
|
+
const canJump = done && !state.demo.running;
|
|
417
|
+
return `<div class="aser-demo-step ${done ? 'done' : ''} ${active ? 'active' : ''} ${canJump ? 'can-jump' : ''}" data-aser-demo-step-index="${index}">${safeText(step.label)}</div>`;
|
|
418
|
+
}).join('');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (logsEl instanceof HTMLElement) {
|
|
422
|
+
const logs = state.demo.logs.slice(-8);
|
|
423
|
+
logsEl.innerHTML = logs.length
|
|
424
|
+
? logs.map((log) => `<div class="aser-demo-log">${safeText(log)}</div>`).join('')
|
|
425
|
+
: (replayEligible
|
|
426
|
+
? '<div class="aser-demo-log">暂无重放日志。</div>'
|
|
427
|
+
: '<div class="aser-demo-log">全景模式不展示重放日志。</div>');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function renderProjectList() {
|
|
432
|
+
const list = byId('aser-project-list');
|
|
433
|
+
const pager = byId('aser-project-list-pager');
|
|
434
|
+
if (!(list instanceof HTMLElement)) return;
|
|
435
|
+
const writeDisabled = state.readonly || !state.hasAccess;
|
|
436
|
+
if (!state.hasAccess) {
|
|
437
|
+
list.innerHTML = '<div class="empty-state">当前权限不可访问 ASER 子系统。</div>';
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (!state.projects.length) {
|
|
441
|
+
list.innerHTML = '<div class="empty-state">暂无 ASER 项目,先创建一个项目开始。</div>';
|
|
442
|
+
if (pager instanceof HTMLElement) pager.innerHTML = '';
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const filteredProjects = state.projects.filter((project) => {
|
|
446
|
+
const pid = String(project?.protocol?.id || '');
|
|
447
|
+
return !state.selectedProtocolPresetId || pid === String(state.selectedProtocolPresetId);
|
|
448
|
+
});
|
|
449
|
+
if (!filteredProjects.length) {
|
|
450
|
+
list.innerHTML = '<div class="empty-state">当前流程下暂无项目,可基于该流程新建 Project。</div>';
|
|
451
|
+
if (pager instanceof HTMLElement) pager.innerHTML = '';
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const pageSize = Math.max(1, Number(state.projectListPageSize || 6));
|
|
455
|
+
const totalPages = Math.max(1, Math.ceil(filteredProjects.length / pageSize));
|
|
456
|
+
const currentPage = Math.max(1, Math.min(totalPages, Number(state.projectListPage || 1)));
|
|
457
|
+
state.projectListPage = currentPage;
|
|
458
|
+
const start = (currentPage - 1) * pageSize;
|
|
459
|
+
const pageProjects = filteredProjects.slice(start, start + pageSize);
|
|
460
|
+
|
|
461
|
+
list.innerHTML = pageProjects.map((project) => {
|
|
462
|
+
const active = String(project.id) === String(state.selectedProjectId) ? ' active' : '';
|
|
463
|
+
return `
|
|
464
|
+
<div class="aser-project-item${active}" data-aser-project-id="${safeText(project.id)}">
|
|
465
|
+
<button type="button" class="aser-project-main-btn" data-aser-project-open-id="${safeText(project.id)}">
|
|
466
|
+
<span class="aser-project-name">${safeText(project.name)}</span>
|
|
467
|
+
<span class="aser-project-meta">protocol: ${safeText(project?.protocol?.id || '-')}</span>
|
|
468
|
+
<span class="aser-project-meta">更新:${safeText(formatDateTime(project.updated_at))}</span>
|
|
469
|
+
</button>
|
|
470
|
+
<button type="button" class="btn btn-secondary btn-sm aser-project-delete-btn" title="删除项目" aria-label="删除项目" data-aser-project-delete-id="${safeText(project.id)}" ${writeDisabled ? 'disabled' : ''}>🗑</button>
|
|
471
|
+
</div>
|
|
472
|
+
`;
|
|
473
|
+
}).join('');
|
|
474
|
+
if (pager instanceof HTMLElement) {
|
|
475
|
+
pager.innerHTML = `
|
|
476
|
+
<button type="button" class="btn btn-secondary btn-sm" data-aser-project-page-action="prev" ${currentPage <= 1 ? 'disabled' : ''}>上一页</button>
|
|
477
|
+
<span class="aser-project-pager-text">第 ${safeText(String(currentPage))} / ${safeText(String(totalPages))} 页</span>
|
|
478
|
+
<button type="button" class="btn btn-secondary btn-sm" data-aser-project-page-action="next" ${currentPage >= totalPages ? 'disabled' : ''}>下一页</button>
|
|
479
|
+
`;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function inferRoleFromText(rawValue) {
|
|
484
|
+
const text = String(rawValue || '').toLowerCase();
|
|
485
|
+
if (!text) return 'system';
|
|
486
|
+
if (text.includes('pm') || text.includes('product_manager') || text.includes('产品')) return 'product_manager';
|
|
487
|
+
if (text.includes('arch') || text.includes('architect') || text.includes('架构')) return 'architect';
|
|
488
|
+
if (text.includes('dev') || text.includes('developer') || text.includes('开发')) return 'developer';
|
|
489
|
+
if (text.includes('rev') || text.includes('review') || text.includes('评审')) return 'reviewer';
|
|
490
|
+
return 'system';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function renderProjectSummary() {
|
|
494
|
+
const detail = byId('aser-project-detail');
|
|
495
|
+
const title = byId('aser-selected-project-title');
|
|
496
|
+
const desc = byId('aser-selected-project-description');
|
|
497
|
+
const updated = byId('aser-selected-project-updated');
|
|
498
|
+
const runtimeState = byId('aser-selected-project-state');
|
|
499
|
+
const threadState = byId('aser-selected-thread-state');
|
|
500
|
+
if (!(detail instanceof HTMLElement)
|
|
501
|
+
|| !(title instanceof HTMLElement)
|
|
502
|
+
|| !(desc instanceof HTMLElement)
|
|
503
|
+
|| !(updated instanceof HTMLElement)
|
|
504
|
+
|| !(runtimeState instanceof HTMLElement)
|
|
505
|
+
|| !(threadState instanceof HTMLElement)) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const selected = state.selectedProject;
|
|
509
|
+
if (!selected) {
|
|
510
|
+
title.textContent = '未选择项目';
|
|
511
|
+
desc.textContent = '从左侧选择或新建一个项目。';
|
|
512
|
+
updated.textContent = '-';
|
|
513
|
+
runtimeState.textContent = '-';
|
|
514
|
+
threadState.textContent = '-';
|
|
515
|
+
detail.classList.add('is-empty');
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
title.textContent = selected.name || '-';
|
|
519
|
+
desc.textContent = selected.description || '暂无描述';
|
|
520
|
+
updated.textContent = formatDateTime(selected.updated_at);
|
|
521
|
+
const projectState = state.selectedProjectState;
|
|
522
|
+
runtimeState.textContent = projectState
|
|
523
|
+
? `完成度 ${projectState.feature_completion}% | blocked ${projectState.blocked_tasks} | review pending ${projectState.review_pending_tasks} | failed ${projectState.failed_tasks} | gate blocked ${projectState.quality_gate_blocked_events || 0}`
|
|
524
|
+
: '-';
|
|
525
|
+
const groups = buildThreadGroups();
|
|
526
|
+
const loopTasks = getTasks().filter((task) => {
|
|
527
|
+
const intent = (state.selectedProject?.intents || []).find((row) => String(row.id) === String(task.intent_id));
|
|
528
|
+
return ['clarify_design', 'escalate_issue', 'report_issue'].includes(String(intent?.taxonomy || ''));
|
|
529
|
+
}).length;
|
|
530
|
+
const assignedTeamId = getAssignedTeamId(selected.id);
|
|
531
|
+
const assignedTeam = getTeamById(assignedTeamId);
|
|
532
|
+
const teamLabel = assignedTeam ? `${assignedTeam.name} (${assignedTeam.id})` : '未分派';
|
|
533
|
+
threadState.textContent = `线程 ${groups.length} 条 | 回路任务 ${loopTasks} 个 | Team ${teamLabel}`;
|
|
534
|
+
detail.classList.remove('is-empty');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function buildStageStats() {
|
|
538
|
+
const intents = Array.isArray(state.selectedProject?.intents) ? state.selectedProject.intents : [];
|
|
539
|
+
const tasks = getTasks();
|
|
540
|
+
const acceptedByIntent = {};
|
|
541
|
+
tasks.forEach((task) => {
|
|
542
|
+
const intent = intents.find((row) => String(row.id) === String(task.intent_id));
|
|
543
|
+
const taxonomy = String(intent?.taxonomy || '').trim();
|
|
544
|
+
if (!taxonomy) return;
|
|
545
|
+
acceptedByIntent[taxonomy] = acceptedByIntent[taxonomy] || { total: 0, accepted: 0, blocked: 0 };
|
|
546
|
+
acceptedByIntent[taxonomy].total += 1;
|
|
547
|
+
if (String(task.status) === 'accepted') acceptedByIntent[taxonomy].accepted += 1;
|
|
548
|
+
if (String(task.status) === 'blocked') acceptedByIntent[taxonomy].blocked += 1;
|
|
549
|
+
});
|
|
550
|
+
return acceptedByIntent;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function buildProtocolCoverageByIntent() {
|
|
554
|
+
const map = {};
|
|
555
|
+
const intents = Array.isArray(state.selectedProject?.intents) ? state.selectedProject.intents : [];
|
|
556
|
+
const tasks = getTasks();
|
|
557
|
+
tasks.forEach((task) => {
|
|
558
|
+
const intent = intents.find((row) => String(row.id) === String(task.intent_id));
|
|
559
|
+
const taxonomy = String(intent?.taxonomy || '').trim();
|
|
560
|
+
if (!taxonomy) return;
|
|
561
|
+
if (!map[taxonomy]) {
|
|
562
|
+
map[taxonomy] = { total: 0, done: 0, active: 0, blocked: 0 };
|
|
563
|
+
}
|
|
564
|
+
map[taxonomy].total += 1;
|
|
565
|
+
const status = String(task.status || '');
|
|
566
|
+
if (status === 'accepted' || status === 'completed') {
|
|
567
|
+
map[taxonomy].done += 1;
|
|
568
|
+
} else if (status === 'blocked' || status === 'failed') {
|
|
569
|
+
map[taxonomy].blocked += 1;
|
|
570
|
+
} else {
|
|
571
|
+
map[taxonomy].active += 1;
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
return map;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function buildThreadGroups() {
|
|
578
|
+
const tasks = getTasks();
|
|
579
|
+
const map = new Map(tasks.map((task) => [String(task.id), task]));
|
|
580
|
+
const taxonomyByTaskId = buildIntentTaxonomyByTaskId();
|
|
581
|
+
const childrenMap = new Map();
|
|
582
|
+
tasks.forEach((task) => {
|
|
583
|
+
const parent = String(task.parent || '');
|
|
584
|
+
if (!parent || !map.has(parent)) return;
|
|
585
|
+
if (!childrenMap.has(parent)) childrenMap.set(parent, []);
|
|
586
|
+
childrenMap.get(parent).push(task);
|
|
587
|
+
});
|
|
588
|
+
const roots = tasks.filter((task) => !task.parent || !map.has(String(task.parent)));
|
|
589
|
+
const groups = roots.map((root) => {
|
|
590
|
+
const queue = [root];
|
|
591
|
+
const members = [];
|
|
592
|
+
while (queue.length) {
|
|
593
|
+
const current = queue.shift();
|
|
594
|
+
members.push(current);
|
|
595
|
+
const children = childrenMap.get(String(current.id)) || [];
|
|
596
|
+
children.forEach((child) => queue.push(child));
|
|
597
|
+
}
|
|
598
|
+
const blocked = members.filter((task) => ['blocked', 'failed'].includes(String(task.status))).length;
|
|
599
|
+
const done = members.filter((task) => ['accepted', 'completed'].includes(String(task.status))).length;
|
|
600
|
+
const active = Math.max(0, members.length - blocked - done);
|
|
601
|
+
const rework = members.filter((task) => String(task.status) === 'failed').length;
|
|
602
|
+
const escalations = members.filter((task) => taxonomyByTaskId[String(task.id)] === 'escalate_issue').length;
|
|
603
|
+
const clarifies = members.filter((task) => taxonomyByTaskId[String(task.id)] === 'clarify_design').length;
|
|
604
|
+
const roundTrips = rework + escalations + clarifies;
|
|
605
|
+
const firstAt = [...members]
|
|
606
|
+
.sort((a, b) => String(a.created_at || '').localeCompare(String(b.created_at || '')))[0]?.created_at || null;
|
|
607
|
+
const lastAt = [...members]
|
|
608
|
+
.sort((a, b) => String(b.updated_at || '').localeCompare(String(a.updated_at || '')))[0]?.updated_at || null;
|
|
609
|
+
const firstMs = firstAt ? Date.parse(String(firstAt)) : 0;
|
|
610
|
+
const lastMs = lastAt ? Date.parse(String(lastAt)) : 0;
|
|
611
|
+
const durationHours = firstMs > 0 && lastMs > 0 && lastMs >= firstMs
|
|
612
|
+
? Number(((lastMs - firstMs) / 3600000).toFixed(2))
|
|
613
|
+
: 0;
|
|
614
|
+
const completionRate = members.length > 0 ? done / members.length : 0;
|
|
615
|
+
const healthScore = clampNumber(
|
|
616
|
+
Math.round(
|
|
617
|
+
100
|
|
618
|
+
- blocked * 18
|
|
619
|
+
- rework * 12
|
|
620
|
+
- escalations * 8
|
|
621
|
+
- clarifies * 5
|
|
622
|
+
+ completionRate * 20,
|
|
623
|
+
),
|
|
624
|
+
0,
|
|
625
|
+
100,
|
|
626
|
+
);
|
|
627
|
+
const health = healthScore >= 80 ? 'healthy' : healthScore >= 60 ? 'watch' : 'risky';
|
|
628
|
+
const healthLabel = health === 'healthy' ? '健康' : health === 'watch' ? '关注' : '高风险';
|
|
629
|
+
return {
|
|
630
|
+
rootId: String(root.id),
|
|
631
|
+
title: root.title || root.id,
|
|
632
|
+
size: members.length,
|
|
633
|
+
blocked,
|
|
634
|
+
done,
|
|
635
|
+
active,
|
|
636
|
+
rework,
|
|
637
|
+
escalations,
|
|
638
|
+
clarifies,
|
|
639
|
+
roundTrips,
|
|
640
|
+
durationHours,
|
|
641
|
+
completionRate,
|
|
642
|
+
healthScore,
|
|
643
|
+
health,
|
|
644
|
+
healthLabel,
|
|
645
|
+
members,
|
|
646
|
+
};
|
|
647
|
+
});
|
|
648
|
+
return groups.sort((a, b) => b.size - a.size);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function buildIntentTaxonomyByTaskId() {
|
|
652
|
+
const intents = Array.isArray(state.selectedProject?.intents) ? state.selectedProject.intents : [];
|
|
653
|
+
const tasks = getTasks();
|
|
654
|
+
const map = {};
|
|
655
|
+
tasks.forEach((task) => {
|
|
656
|
+
const intent = intents.find((row) => String(row.id) === String(task.intent_id));
|
|
657
|
+
map[String(task.id)] = String(intent?.taxonomy || '');
|
|
658
|
+
});
|
|
659
|
+
return map;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function renderProtocolVisual() {
|
|
663
|
+
const protocolVisual = byId('aser-protocol-visual');
|
|
664
|
+
const protocolFlow = byId('aser-protocol-flow');
|
|
665
|
+
const protocolFullscreen = byId('aser-protocol-flow-fullscreen');
|
|
666
|
+
const protocolInput = byId('aser-protocol-json-input');
|
|
667
|
+
const graphBtn = byId('aser-protocol-view-graph-btn');
|
|
668
|
+
const textBtn = byId('aser-protocol-view-text-btn');
|
|
669
|
+
const coverageToggle = byId('aser-protocol-coverage-toggle');
|
|
670
|
+
if (!(protocolVisual instanceof HTMLElement) || !(protocolFlow instanceof HTMLElement)) return;
|
|
671
|
+
if (graphBtn instanceof HTMLButtonElement) graphBtn.classList.toggle('active', state.protocolViewMode === 'graph');
|
|
672
|
+
if (textBtn instanceof HTMLButtonElement) textBtn.classList.toggle('active', state.protocolViewMode === 'text');
|
|
673
|
+
if (coverageToggle instanceof HTMLInputElement) {
|
|
674
|
+
coverageToggle.checked = Boolean(state.protocolCoverageEnabled);
|
|
675
|
+
coverageToggle.disabled = !state.selectedProjectId;
|
|
676
|
+
}
|
|
677
|
+
const activeProtocol = getActiveProtocol();
|
|
678
|
+
if (protocolInput instanceof HTMLTextAreaElement) {
|
|
679
|
+
protocolInput.value = activeProtocol ? JSON.stringify(activeProtocol, null, 2) : '';
|
|
680
|
+
}
|
|
681
|
+
if (!activeProtocol) {
|
|
682
|
+
protocolVisual.innerHTML = '<div class="empty-state">未加载 Protocol。</div>';
|
|
683
|
+
protocolFlow.innerHTML = '<div class="empty-state">未加载流程定义。</div>';
|
|
684
|
+
if (protocolFullscreen instanceof HTMLElement) {
|
|
685
|
+
protocolFullscreen.innerHTML = '<div class="empty-state">未加载流程定义。</div>';
|
|
686
|
+
}
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const roles = Array.isArray(activeProtocol.roles) ? activeProtocol.roles : [];
|
|
690
|
+
const rules = Array.isArray(activeProtocol.intent_rules) ? activeProtocol.intent_rules : [];
|
|
691
|
+
const intents = Array.isArray(activeProtocol.intents) ? activeProtocol.intents : [];
|
|
692
|
+
const coverage = buildProtocolCoverageByIntent();
|
|
693
|
+
const overlayOn = Boolean(state.protocolCoverageEnabled && state.selectedProjectId);
|
|
694
|
+
protocolVisual.innerHTML = `
|
|
695
|
+
<div class="aser-protocol-meta">id: ${safeText(activeProtocol.id || '-')} | version: ${safeText(String(activeProtocol.version || '-'))}</div>
|
|
696
|
+
<div class="aser-protocol-block">
|
|
697
|
+
<div class="aser-protocol-block-title">Roles</div>
|
|
698
|
+
${(roles.length
|
|
699
|
+
? roles.map((role) => `<div class="aser-protocol-line"><strong>${safeText(role.role_id)}</strong>: ${safeText((role.intents || []).join(', '))}</div>`).join('')
|
|
700
|
+
: '<div class="aser-protocol-line">无</div>')}
|
|
701
|
+
</div>
|
|
702
|
+
<div class="aser-protocol-block">
|
|
703
|
+
<div class="aser-protocol-block-title">Intent Rules</div>
|
|
704
|
+
${(rules.length
|
|
705
|
+
? rules.map((rule) => {
|
|
706
|
+
const stats = coverage[String(rule.intent || '')] || { total: 0, done: 0, active: 0, blocked: 0 };
|
|
707
|
+
const coverageStatus = protocolCoverageStatus(stats);
|
|
708
|
+
const coverageText = overlayOn
|
|
709
|
+
? `<span class="aser-protocol-coverage-meta ${safeText(`is-${coverageStatus.key}`)}">状态 ${safeText(coverageStatus.label)}</span>`
|
|
710
|
+
: '';
|
|
711
|
+
return `<div class="aser-protocol-line">${safeText(rule.intent)} <= ${safeText((rule.requires || []).join(', ') || 'none')} ${coverageText}</div>`;
|
|
712
|
+
}).join('')
|
|
713
|
+
: '<div class="aser-protocol-line">无</div>')}
|
|
714
|
+
</div>
|
|
715
|
+
`;
|
|
716
|
+
protocolVisual.classList.toggle('hidden', state.protocolViewMode !== 'text');
|
|
717
|
+
const stageRows = PM_STAGE_ORDER
|
|
718
|
+
.filter((intent) => intents.includes(intent))
|
|
719
|
+
.map((intent, index) => {
|
|
720
|
+
const requires = rules.find((row) => row.intent === intent)?.requires || [];
|
|
721
|
+
const stats = coverage[intent] || { total: 0, done: 0, active: 0, blocked: 0 };
|
|
722
|
+
const coverageStatus = protocolCoverageStatus(stats);
|
|
723
|
+
const prev = index <= 0 ? '' : '<span class="aser-flow-arrow">→</span>';
|
|
724
|
+
let className = intent === 'clarify_design'
|
|
725
|
+
? 'is-clarify'
|
|
726
|
+
: intent === 'escalate_issue'
|
|
727
|
+
? 'is-escalate'
|
|
728
|
+
: '';
|
|
729
|
+
if (overlayOn) {
|
|
730
|
+
if (stats.blocked > 0) className += ' is-coverage-risk';
|
|
731
|
+
else if (stats.active > 0) className += ' is-coverage-active';
|
|
732
|
+
else if (stats.done > 0) className += ' is-coverage-done';
|
|
733
|
+
}
|
|
734
|
+
const coverageLine = overlayOn
|
|
735
|
+
? `<span class="aser-flow-node-status aser-status-${safeText(coverageStatus.key)}" title="${safeText(`状态: ${coverageStatus.label}`)}"></span>`
|
|
736
|
+
: '';
|
|
737
|
+
return `
|
|
738
|
+
${prev}
|
|
739
|
+
<span class="aser-flow-node ${className}">
|
|
740
|
+
<span>${safeText(PM_STAGE_LABELS[intent] || intent)}</span>
|
|
741
|
+
<small class="aser-flow-require">依赖: ${safeText(requires.join(', ') || 'none')}</small>
|
|
742
|
+
${coverageLine}
|
|
743
|
+
</span>
|
|
744
|
+
`;
|
|
745
|
+
}).join('');
|
|
746
|
+
protocolFlow.innerHTML = stageRows || '<div class="empty-state">当前协议未定义可展示的流程节点。</div>';
|
|
747
|
+
protocolFlow.classList.toggle('hidden', state.protocolViewMode !== 'graph');
|
|
748
|
+
|
|
749
|
+
if (protocolFullscreen instanceof HTMLElement) {
|
|
750
|
+
const detailFlow = PM_STAGE_ORDER
|
|
751
|
+
.filter((intent) => intents.includes(intent))
|
|
752
|
+
.map((intent, index) => {
|
|
753
|
+
const requires = rules.find((row) => row.intent === intent)?.requires || [];
|
|
754
|
+
const stats = coverage[intent] || { total: 0, done: 0, active: 0, blocked: 0 };
|
|
755
|
+
const coverageStatus = protocolCoverageStatus(stats);
|
|
756
|
+
const prev = index <= 0 ? '' : '<span class="aser-flow-arrow detail">→</span>';
|
|
757
|
+
const statusTag = overlayOn
|
|
758
|
+
? `<span class="aser-protocol-coverage-meta ${safeText(`is-${coverageStatus.key}`)}">${safeText(coverageStatus.label)}</span>`
|
|
759
|
+
: '';
|
|
760
|
+
let className = 'detail';
|
|
761
|
+
if (overlayOn) {
|
|
762
|
+
if (stats.blocked > 0) className += ' is-coverage-risk';
|
|
763
|
+
else if (stats.active > 0) className += ' is-coverage-active';
|
|
764
|
+
else if (stats.done > 0) className += ' is-coverage-done';
|
|
765
|
+
}
|
|
766
|
+
return `
|
|
767
|
+
${prev}
|
|
768
|
+
<span class="aser-flow-node ${className}">
|
|
769
|
+
<div class="aser-flow-node-head">
|
|
770
|
+
<strong>${safeText(PM_STAGE_LABELS[intent] || intent)}</strong>
|
|
771
|
+
${statusTag}
|
|
772
|
+
</div>
|
|
773
|
+
<small>intent: ${safeText(intent)}</small>
|
|
774
|
+
<small class="aser-flow-require">依赖: ${safeText(requires.join(', ') || 'none')}</small>
|
|
775
|
+
<small>次数 ${safeText(String(stats.total))} | 完成 ${safeText(String(stats.done))} | 进行 ${safeText(String(stats.active))} | 阻塞 ${safeText(String(stats.blocked))}</small>
|
|
776
|
+
</span>
|
|
777
|
+
`;
|
|
778
|
+
}).join('');
|
|
779
|
+
protocolFullscreen.innerHTML = detailFlow
|
|
780
|
+
? `<div class="aser-protocol-flow-detailed">${detailFlow}</div>`
|
|
781
|
+
: '<div class="empty-state">当前协议未定义可展示的流程节点。</div>';
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function renderPmBoard() {
|
|
786
|
+
const board = byId('aser-pm-board');
|
|
787
|
+
if (!(board instanceof HTMLElement)) return;
|
|
788
|
+
if (!state.selectedProject) {
|
|
789
|
+
board.innerHTML = '<div class="empty-state">选择项目后展示 PM 过程看板。</div>';
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const stageStats = buildStageStats();
|
|
793
|
+
const cards = PM_STAGE_ORDER.map((intent) => {
|
|
794
|
+
const stats = stageStats[intent] || { total: 0, accepted: 0, blocked: 0 };
|
|
795
|
+
const riskClass = stats.blocked > 0 || intent === 'escalate_issue' ? 'risk' : '';
|
|
796
|
+
return `
|
|
797
|
+
<div class="aser-pm-card ${riskClass}">
|
|
798
|
+
<div class="aser-pm-card-title">${safeText(PM_STAGE_LABELS[intent])}</div>
|
|
799
|
+
<div class="aser-pm-card-meta">任务: ${safeText(String(stats.total))}</div>
|
|
800
|
+
<div class="aser-pm-card-meta">通过: ${safeText(String(stats.accepted))}</div>
|
|
801
|
+
<div class="aser-pm-card-meta">阻塞: ${safeText(String(stats.blocked))}</div>
|
|
802
|
+
</div>
|
|
803
|
+
`;
|
|
804
|
+
}).join('');
|
|
805
|
+
board.innerHTML = `
|
|
806
|
+
<div class="aser-pm-kpi-row">
|
|
807
|
+
<div class="aser-pm-kpi">澄清任务: ${safeText(String(stageStats.clarify_design?.total || 0))}</div>
|
|
808
|
+
<div class="aser-pm-kpi">上升任务: ${safeText(String(stageStats.escalate_issue?.total || 0))}</div>
|
|
809
|
+
<div class="aser-pm-kpi">问题记录: ${safeText(String(stageStats.report_issue?.total || 0))}</div>
|
|
810
|
+
</div>
|
|
811
|
+
<div class="aser-pm-card-grid">${cards}</div>
|
|
812
|
+
`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function renderThreadSummary() {
|
|
816
|
+
const panel = byId('aser-thread-summary');
|
|
817
|
+
if (!(panel instanceof HTMLElement)) return;
|
|
818
|
+
if (!state.selectedProject) {
|
|
819
|
+
panel.innerHTML = '<div class="empty-state">选择项目后展示 thread 摘要。</div>';
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const groups = buildThreadGroups();
|
|
823
|
+
if (!groups.length) {
|
|
824
|
+
panel.innerHTML = '<div class="empty-state">当前项目暂无 thread 数据。</div>';
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
panel.innerHTML = groups.map((group) => `
|
|
828
|
+
<article class="aser-thread-summary-row" data-aser-thread-root-id="${safeText(group.rootId)}">
|
|
829
|
+
<div class="aser-thread-summary-main">
|
|
830
|
+
<span>${safeText(group.title)}</span>
|
|
831
|
+
<span class="aser-health-badge ${safeText(group.health)}">${safeText(`${group.healthLabel} ${group.healthScore}`)}</span>
|
|
832
|
+
</div>
|
|
833
|
+
<div class="aser-thread-summary-meta">任务 ${safeText(String(group.size))} | 完成 ${safeText(String(group.done))} | 进行 ${safeText(String(group.active))} | 阻塞 ${safeText(String(group.blocked))}</div>
|
|
834
|
+
<div class="aser-thread-summary-meta">回路 ${safeText(String(group.roundTrips))} | 上升 ${safeText(String(group.escalations))} | 返工 ${safeText(String(group.rework))} | 用时 ${safeText(String(group.durationHours))}h</div>
|
|
835
|
+
</article>
|
|
836
|
+
`).join('');
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function renderGovernanceAdvice() {
|
|
840
|
+
const panel = byId('aser-governance-advice');
|
|
841
|
+
if (!(panel instanceof HTMLElement)) return;
|
|
842
|
+
if (!state.selectedProject) {
|
|
843
|
+
panel.innerHTML = '<div class="empty-state">选择项目后显示治理建议。</div>';
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const groups = buildThreadGroups();
|
|
847
|
+
if (!groups.length) {
|
|
848
|
+
panel.innerHTML = '<div class="empty-state">暂无 thread 数据。</div>';
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const sorted = [...groups].sort((a, b) => {
|
|
852
|
+
const scoreA = a.healthScore - a.blocked * 8 - a.roundTrips * 3;
|
|
853
|
+
const scoreB = b.healthScore - b.blocked * 8 - b.roundTrips * 3;
|
|
854
|
+
return scoreA - scoreB;
|
|
855
|
+
});
|
|
856
|
+
const top = sorted[0];
|
|
857
|
+
const eventRows = Array.isArray(state.selectedProject?.events) ? state.selectedProject.events : [];
|
|
858
|
+
const topMemberIds = new Set((top.members || []).map((row) => String(row.id)));
|
|
859
|
+
const gateBlockedCount = eventRows.filter((row) => {
|
|
860
|
+
if (String(row.type || '') !== 'quality_gate_blocked') return false;
|
|
861
|
+
const taskId = String(row.payload?.task_id || '');
|
|
862
|
+
return Boolean(taskId) && topMemberIds.has(taskId);
|
|
863
|
+
}).length;
|
|
864
|
+
const reasons = [];
|
|
865
|
+
if (top.blocked > 0) reasons.push(`阻塞 ${top.blocked}`);
|
|
866
|
+
if (top.rework > 0) reasons.push(`返工 ${top.rework}`);
|
|
867
|
+
if (top.escalations > 0) reasons.push(`上升 ${top.escalations}`);
|
|
868
|
+
if (top.clarifies > 0) reasons.push(`澄清 ${top.clarifies}`);
|
|
869
|
+
if (gateBlockedCount > 0) reasons.push(`闸门拦截 ${gateBlockedCount}`);
|
|
870
|
+
const reasonText = reasons.length ? reasons.join(' / ') : '线程存在波动';
|
|
871
|
+
panel.innerHTML = `
|
|
872
|
+
<div class="aser-governance-card">
|
|
873
|
+
<div class="aser-governance-title">优先治理线程</div>
|
|
874
|
+
<div class="aser-governance-main">${safeText(top.title)}</div>
|
|
875
|
+
<div class="aser-governance-meta">健康度:${safeText(String(top.healthScore))}(${safeText(top.healthLabel)})</div>
|
|
876
|
+
<div class="aser-governance-meta">原因:${safeText(reasonText)}</div>
|
|
877
|
+
<div class="aser-governance-meta">闸门拦截:${safeText(String(gateBlockedCount))}</div>
|
|
878
|
+
<div class="aser-governance-meta">建议:先处理阻塞来源,再收敛回路任务,最后关闭上升项。</div>
|
|
879
|
+
</div>
|
|
880
|
+
`;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function renderPulledTask() {
|
|
884
|
+
const pulledTaskPreview = byId('aser-pulled-task-preview');
|
|
885
|
+
if (!(pulledTaskPreview instanceof HTMLElement)) return;
|
|
886
|
+
if (!state.pulledTask?.task) {
|
|
887
|
+
pulledTaskPreview.textContent = '暂无拉取结果';
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
pulledTaskPreview.textContent = `task: ${state.pulledTask.task.title}
|
|
891
|
+
status: ${state.pulledTask.task.status}
|
|
892
|
+
owner: ${state.pulledTask.task.owner_role}`;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function renderChatProcess() {
|
|
896
|
+
const container = byId('aser-chat-process-list');
|
|
897
|
+
if (!(container instanceof HTMLElement)) return;
|
|
898
|
+
const tasks = getTasks();
|
|
899
|
+
const intents = Array.isArray(state.selectedProject?.intents) ? state.selectedProject.intents : [];
|
|
900
|
+
const evaluations = Array.isArray(state.selectedProject?.evaluations) ? state.selectedProject.evaluations : [];
|
|
901
|
+
const artifacts = Array.isArray(state.selectedProject?.artifacts) ? state.selectedProject.artifacts : [];
|
|
902
|
+
const events = Array.isArray(state.selectedProject?.events) ? state.selectedProject.events : [];
|
|
903
|
+
if (!tasks.length) {
|
|
904
|
+
container.innerHTML = '<div class="empty-state">暂无任务过程,先提交事件驱动任务生成。</div>';
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const taskById = new Map(tasks.map((task) => [String(task.id), task]));
|
|
908
|
+
const intentById = new Map(intents.map((intent) => [String(intent.id), intent]));
|
|
909
|
+
|
|
910
|
+
function buildTaskInteractionRows(task) {
|
|
911
|
+
const taskEvents = events
|
|
912
|
+
.filter((row) => {
|
|
913
|
+
const payload = row?.payload || {};
|
|
914
|
+
const direct = String(payload?.task_id || '') === String(task.id);
|
|
915
|
+
const parent = String(payload?.parent || '') === String(task.id);
|
|
916
|
+
const related = Array.isArray(payload?.related) && payload.related.map((v) => String(v)).includes(String(task.id));
|
|
917
|
+
const byIntent = String(payload?.intent_title || '').includes(String(task.title || ''));
|
|
918
|
+
return direct || parent || related || byIntent;
|
|
919
|
+
})
|
|
920
|
+
.map((row) => ({
|
|
921
|
+
ts: row.timestamp || row.created_at || '',
|
|
922
|
+
type: 'event',
|
|
923
|
+
label: `event: ${String(row.type || '-')}`,
|
|
924
|
+
detail: `actor ${String(row.actor || '-')} (${String(row.actor_kind || '-')})`,
|
|
925
|
+
}));
|
|
926
|
+
const taskArtifacts = artifacts
|
|
927
|
+
.filter((row) => String(row.task_id) === String(task.id))
|
|
928
|
+
.map((row) => ({
|
|
929
|
+
ts: row.created_at || '',
|
|
930
|
+
type: 'artifact',
|
|
931
|
+
label: `artifact: ${String(row.type || '-')}`,
|
|
932
|
+
detail: String(row.summary || row.uri || '-'),
|
|
933
|
+
}));
|
|
934
|
+
const taskEvaluations = evaluations
|
|
935
|
+
.filter((row) => String(row.task_id) === String(task.id))
|
|
936
|
+
.map((row) => {
|
|
937
|
+
const decision = String(row.decision || '-');
|
|
938
|
+
const normalized = decision === 'accept' ? 'approved' : decision;
|
|
939
|
+
return {
|
|
940
|
+
ts: row.created_at || '',
|
|
941
|
+
type: 'evaluation',
|
|
942
|
+
label: `state: ${normalized}`,
|
|
943
|
+
detail: `by ${String(row.evaluator || '-')} | ${String(row.comments || '-')}`,
|
|
944
|
+
};
|
|
945
|
+
});
|
|
946
|
+
return [...taskEvents, ...taskArtifacts, ...taskEvaluations]
|
|
947
|
+
.sort((a, b) => String(a.ts).localeCompare(String(b.ts)))
|
|
948
|
+
.slice(-10);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
container.innerHTML = tasks.map((task) => {
|
|
952
|
+
const intent = intents.find((row) => String(row.id) === String(task.intent_id));
|
|
953
|
+
const stage = PM_STAGE_LABELS[String(intent?.taxonomy || '')] || String(intent?.taxonomy || 'unknown');
|
|
954
|
+
const role = String(task.owner_role || 'developer');
|
|
955
|
+
const roleStyle = ROLE_STYLE[role] || { label: role.toUpperCase().slice(0, 4), tone: 'dev' };
|
|
956
|
+
const threadHint = task.parent ? `thread: reply to ${task.parent}` : `thread root: ${task.id}`;
|
|
957
|
+
const parentTask = task.parent ? taskById.get(String(task.parent)) : null;
|
|
958
|
+
const upstreamIntent = parentTask ? intentById.get(String(parentTask.intent_id || '')) : null;
|
|
959
|
+
const interactionRows = buildTaskInteractionRows(task);
|
|
960
|
+
return `
|
|
961
|
+
<article class="aser-chat-task ${state.focusedEntity?.taskId === task.id ? 'active' : ''}" data-aser-chat-task-id="${safeText(task.id)}">
|
|
962
|
+
<div class="aser-chat-task-head">
|
|
963
|
+
<span class="aser-role-badge ${safeText(roleStyle.tone)}">${safeText(roleStyle.label)}</span>
|
|
964
|
+
<span class="aser-chat-task-title">${safeText(task.title || task.id)}</span>
|
|
965
|
+
<span class="aser-chat-task-status ${taskStatusClass(task.status)}">${safeText(task.status || 'created')}</span>
|
|
966
|
+
</div>
|
|
967
|
+
<div class="aser-chat-task-meta">阶段: ${safeText(stage)}</div>
|
|
968
|
+
<div class="aser-chat-task-meta">owner: ${safeText(task.owner_role || '-')} | agent: ${safeText(task.preferred_agent || '-')}</div>
|
|
969
|
+
<div class="aser-chat-task-meta">驱动链: ${safeText(parentTask ? `${parentTask.owner_role || '-'} -> ${task.owner_role || '-'}` : 'system -> ' + (task.owner_role || '-'))}</div>
|
|
970
|
+
<div class="aser-chat-task-meta">上游任务: ${safeText(parentTask?.title || 'none')} | 上游阶段: ${safeText(PM_STAGE_LABELS[String(upstreamIntent?.taxonomy || '')] || '-')}</div>
|
|
971
|
+
<div class="aser-chat-task-meta">${safeText(threadHint)}</div>
|
|
972
|
+
<div class="aser-chat-task-meta">updated: ${safeText(formatDateTime(task.updated_at))}</div>
|
|
973
|
+
<div class="aser-chat-task-timeline">
|
|
974
|
+
${interactionRows.length
|
|
975
|
+
? interactionRows.map((row) => `
|
|
976
|
+
<div class="aser-chat-task-interaction ${safeText(row.type)}">
|
|
977
|
+
<span>${safeText(formatDateTime(row.ts))}</span>
|
|
978
|
+
<strong>${safeText(row.label)}</strong>
|
|
979
|
+
<span>${safeText(row.detail)}</span>
|
|
980
|
+
</div>
|
|
981
|
+
`).join('')
|
|
982
|
+
: '<div class="aser-chat-task-interaction">暂无交互细节</div>'}
|
|
983
|
+
</div>
|
|
984
|
+
</article>
|
|
985
|
+
`;
|
|
986
|
+
}).join('');
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function renderTaskGraph() {
|
|
990
|
+
const graph = byId('aser-task-graph');
|
|
991
|
+
if (!(graph instanceof HTMLElement)) return;
|
|
992
|
+
const tasks = getTasks();
|
|
993
|
+
if (!tasks.length) {
|
|
994
|
+
graph.innerHTML = '<div class="empty-state">暂无可绘制的任务图。</div>';
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const taskById = new Map(tasks.map((task) => [String(task.id), task]));
|
|
998
|
+
const artifacts = Array.isArray(state.selectedProject?.artifacts) ? state.selectedProject.artifacts : [];
|
|
999
|
+
const evaluations = Array.isArray(state.selectedProject?.evaluations) ? state.selectedProject.evaluations : [];
|
|
1000
|
+
const events = Array.isArray(state.selectedProject?.events) ? state.selectedProject.events : [];
|
|
1001
|
+
const roleLane = ROLE_ORDER.filter((role) => tasks.some((task) => String(task.owner_role) === String(role)));
|
|
1002
|
+
const laneRoles = roleLane.length ? roleLane : ROLE_ORDER.slice(0, 4);
|
|
1003
|
+
const laneCenters = laneRoles.map((_, index) => ((index + 0.5) * 100) / laneRoles.length);
|
|
1004
|
+
const interactions = [];
|
|
1005
|
+
|
|
1006
|
+
tasks.forEach((task) => {
|
|
1007
|
+
const fromTask = task.parent ? taskById.get(String(task.parent)) : null;
|
|
1008
|
+
const fromRole = fromTask ? String(fromTask.owner_role || 'system') : 'system';
|
|
1009
|
+
const toRole = String(task.owner_role || 'developer');
|
|
1010
|
+
interactions.push({
|
|
1011
|
+
id: String(task.id),
|
|
1012
|
+
fromRole,
|
|
1013
|
+
toRole,
|
|
1014
|
+
action: 'created',
|
|
1015
|
+
title: String(task.title || task.id),
|
|
1016
|
+
time: String(task.created_at || ''),
|
|
1017
|
+
});
|
|
1018
|
+
artifacts
|
|
1019
|
+
.filter((row) => String(row.task_id) === String(task.id))
|
|
1020
|
+
.forEach((row) => {
|
|
1021
|
+
interactions.push({
|
|
1022
|
+
id: String(task.id),
|
|
1023
|
+
fromRole: toRole,
|
|
1024
|
+
toRole,
|
|
1025
|
+
action: 'in_progress',
|
|
1026
|
+
title: `${String(task.title || task.id)} | artifact ${String(row.type || '-')}`,
|
|
1027
|
+
time: String(row.created_at || task.updated_at || ''),
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
evaluations
|
|
1031
|
+
.filter((row) => String(row.task_id) === String(task.id))
|
|
1032
|
+
.forEach((row) => {
|
|
1033
|
+
const decision = String(row.decision || '');
|
|
1034
|
+
interactions.push({
|
|
1035
|
+
id: String(task.id),
|
|
1036
|
+
fromRole: inferRoleFromText(row.evaluator),
|
|
1037
|
+
toRole,
|
|
1038
|
+
action: decision === 'accept' ? 'approved' : decision || 'evaluating',
|
|
1039
|
+
title: `${String(task.title || task.id)} | evaluation`,
|
|
1040
|
+
time: String(row.created_at || task.updated_at || ''),
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
1043
|
+
events
|
|
1044
|
+
.filter((row) => String(row.payload?.task_id || '') === String(task.id))
|
|
1045
|
+
.forEach((row) => {
|
|
1046
|
+
interactions.push({
|
|
1047
|
+
id: String(task.id),
|
|
1048
|
+
fromRole: inferRoleFromText(row.actor_role || row.actor),
|
|
1049
|
+
toRole,
|
|
1050
|
+
action: String(row.type || 'event'),
|
|
1051
|
+
title: `${String(task.title || task.id)} | event ${String(row.type || '-')}`,
|
|
1052
|
+
time: String(row.timestamp || row.created_at || task.updated_at || ''),
|
|
1053
|
+
});
|
|
1054
|
+
});
|
|
1055
|
+
const finalStatus = String(task.status || 'created');
|
|
1056
|
+
interactions.push({
|
|
1057
|
+
id: String(task.id),
|
|
1058
|
+
fromRole: toRole,
|
|
1059
|
+
toRole,
|
|
1060
|
+
action: finalStatus === 'accepted' ? 'approved' : finalStatus,
|
|
1061
|
+
title: `${String(task.title || task.id)} | final`,
|
|
1062
|
+
time: String(task.updated_at || task.created_at || ''),
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
interactions.sort((a, b) => String(a.time).localeCompare(String(b.time)));
|
|
1067
|
+
const previousStatus = {};
|
|
1068
|
+
const timeline = interactions.map((row) => {
|
|
1069
|
+
const prev = previousStatus[row.id] || 'none';
|
|
1070
|
+
previousStatus[row.id] = row.action;
|
|
1071
|
+
return {
|
|
1072
|
+
...row,
|
|
1073
|
+
transition: prev === row.action ? row.action : `${prev} -> ${row.action}`,
|
|
1074
|
+
};
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
const laneHeader = laneRoles.map((role) => `
|
|
1078
|
+
<div class="aser-sequence-lane" style="width:${safeText(String(100 / laneRoles.length))}%">
|
|
1079
|
+
<span class="aser-role-badge ${(ROLE_STYLE[role] || { tone: 'dev' }).tone}">${safeText((ROLE_STYLE[role] || { label: role }).label || role)}</span>
|
|
1080
|
+
<small>${safeText(role)}</small>
|
|
1081
|
+
</div>
|
|
1082
|
+
`).join('');
|
|
1083
|
+
|
|
1084
|
+
const rows = timeline.map((row) => {
|
|
1085
|
+
const fromIdx = Math.max(0, laneRoles.indexOf(row.fromRole));
|
|
1086
|
+
const toIdx = Math.max(0, laneRoles.indexOf(row.toRole));
|
|
1087
|
+
const left = laneCenters[Math.min(fromIdx, toIdx)] || laneCenters[0];
|
|
1088
|
+
const right = laneCenters[Math.max(fromIdx, toIdx)] || laneCenters[0];
|
|
1089
|
+
const width = Math.max(2, right - left);
|
|
1090
|
+
const reverse = toIdx < fromIdx ? 'reverse' : '';
|
|
1091
|
+
return `
|
|
1092
|
+
<div class="aser-sequence-row">
|
|
1093
|
+
<div class="aser-sequence-arrow ${safeText(reverse)}" style="left:${left}%; width:${width}%"></div>
|
|
1094
|
+
<button type="button" class="aser-sequence-message ${taskStatusClass(row.action)} ${state.focusedEntity?.taskId === row.id ? 'active' : ''}" data-aser-graph-task-id="${safeText(row.id)}">
|
|
1095
|
+
<span class="aser-sequence-time">${safeText(formatDateTime(row.time))}</span>
|
|
1096
|
+
<strong>${safeText(row.fromRole)} -> ${safeText(row.toRole)}</strong>
|
|
1097
|
+
<span>${safeText(row.title)}</span>
|
|
1098
|
+
<span class="aser-sequence-transition">${safeText(`状态变化: ${row.transition}`)}</span>
|
|
1099
|
+
</button>
|
|
1100
|
+
</div>
|
|
1101
|
+
`;
|
|
1102
|
+
}).join('');
|
|
1103
|
+
graph.innerHTML = `
|
|
1104
|
+
<div class="aser-graph-overview">UML Sequence: 角色泳道 ${laneRoles.length} | 交互 ${timeline.length}</div>
|
|
1105
|
+
<div class="aser-sequence-header">${laneHeader}</div>
|
|
1106
|
+
<div class="aser-sequence-body">${rows}</div>
|
|
1107
|
+
`;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function renderTaskThread() {
|
|
1111
|
+
const thread = byId('aser-task-thread');
|
|
1112
|
+
if (!(thread instanceof HTMLElement)) return;
|
|
1113
|
+
const tasks = getTasks();
|
|
1114
|
+
if (!tasks.length) {
|
|
1115
|
+
thread.innerHTML = '<div class="empty-state">暂无 thread 记录。</div>';
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const taxonomyByTaskId = buildIntentTaxonomyByTaskId();
|
|
1119
|
+
const childrenMap = new Map();
|
|
1120
|
+
tasks.forEach((task) => {
|
|
1121
|
+
const parent = String(task.parent || '');
|
|
1122
|
+
if (!parent) return;
|
|
1123
|
+
if (!childrenMap.has(parent)) childrenMap.set(parent, []);
|
|
1124
|
+
childrenMap.get(parent).push(task);
|
|
1125
|
+
});
|
|
1126
|
+
const roots = tasks
|
|
1127
|
+
.filter((task) => !task.parent || !tasks.some((row) => String(row.id) === String(task.parent)))
|
|
1128
|
+
.sort((a, b) => String(a.created_at || '').localeCompare(String(b.created_at || '')));
|
|
1129
|
+
|
|
1130
|
+
function collectMembers(root) {
|
|
1131
|
+
const queue = [root];
|
|
1132
|
+
const members = [];
|
|
1133
|
+
while (queue.length) {
|
|
1134
|
+
const current = queue.shift();
|
|
1135
|
+
members.push(current);
|
|
1136
|
+
const children = childrenMap.get(String(current.id)) || [];
|
|
1137
|
+
children.forEach((child) => queue.push(child));
|
|
1138
|
+
}
|
|
1139
|
+
return members;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const filteredRoots = roots.filter((root) => {
|
|
1143
|
+
if (state.threadFilter === 'all') return true;
|
|
1144
|
+
const members = collectMembers(root);
|
|
1145
|
+
const taxonomies = members.map((row) => taxonomyByTaskId[String(row.id)]).filter(Boolean);
|
|
1146
|
+
if (state.threadFilter === 'blocked') {
|
|
1147
|
+
return members.some((row) => ['blocked', 'failed'].includes(String(row.status)));
|
|
1148
|
+
}
|
|
1149
|
+
if (state.threadFilter === 'loop') {
|
|
1150
|
+
return taxonomies.some((row) => ['clarify_design', 'escalate_issue', 'report_issue'].includes(String(row)));
|
|
1151
|
+
}
|
|
1152
|
+
if (state.threadFilter === 'rework') {
|
|
1153
|
+
return members.filter((row) => String(row.status) === 'failed').length > 0;
|
|
1154
|
+
}
|
|
1155
|
+
return true;
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
function renderNode(task, depth) {
|
|
1159
|
+
const children = childrenMap.get(String(task.id)) || [];
|
|
1160
|
+
const taxonomy = taxonomyByTaskId[String(task.id)] || '';
|
|
1161
|
+
const stage = PM_STAGE_LABELS[taxonomy] || taxonomy || 'unknown';
|
|
1162
|
+
const rowClass = depth <= 0 ? 'aser-thread-root' : 'aser-thread-child';
|
|
1163
|
+
const flowClass = isLoopTaxonomy(taxonomy) ? 'loop' : 'mainline';
|
|
1164
|
+
const flowLabel = isLoopTaxonomy(taxonomy) ? '回路' : '主线';
|
|
1165
|
+
const childrenRows = children.map((child) => renderNode(child, depth + 1)).join('');
|
|
1166
|
+
return `
|
|
1167
|
+
<div class="${rowClass} ${flowClass}" style="margin-left: ${depth * 14}px" data-aser-thread-task-id="${safeText(task.id)}">
|
|
1168
|
+
<span>${safeText(task.title || task.id)}</span>
|
|
1169
|
+
<span class="aser-thread-related">${safeText(stage)} | ${safeText(String(task.status || 'created'))}</span>
|
|
1170
|
+
<span class="aser-thread-flow-tag ${safeText(flowClass)}">${safeText(flowLabel)}</span>
|
|
1171
|
+
</div>
|
|
1172
|
+
${childrenRows}
|
|
1173
|
+
`;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const lines = filteredRoots.map((root) => {
|
|
1177
|
+
const members = collectMembers(root);
|
|
1178
|
+
const blocked = members.filter((row) => ['blocked', 'failed'].includes(String(row.status))).length;
|
|
1179
|
+
const done = members.filter((row) => ['accepted', 'completed'].includes(String(row.status))).length;
|
|
1180
|
+
const active = Math.max(0, members.length - blocked - done);
|
|
1181
|
+
const riskTag = blocked > 0 ? '<span class="aser-thread-badge risk">risk</span>' : '<span class="aser-thread-badge normal">stable</span>';
|
|
1182
|
+
return `
|
|
1183
|
+
<section class="aser-thread-group">
|
|
1184
|
+
<div class="aser-thread-group-head" data-aser-thread-task-id="${safeText(root.id)}">
|
|
1185
|
+
<strong>${safeText(root.title || root.id)}</strong>
|
|
1186
|
+
<span class="aser-thread-related">任务 ${safeText(String(members.length))} | 完成 ${safeText(String(done))} | 进行 ${safeText(String(active))} | 阻塞 ${safeText(String(blocked))}</span>
|
|
1187
|
+
${riskTag}
|
|
1188
|
+
</div>
|
|
1189
|
+
<div class="aser-thread-group-body">
|
|
1190
|
+
${renderNode(root, 0)}
|
|
1191
|
+
</div>
|
|
1192
|
+
</section>
|
|
1193
|
+
`;
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
thread.innerHTML = lines.join('') || '<div class="empty-state">当前筛选下暂无 thread 记录。</div>';
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function renderModePanels() {
|
|
1200
|
+
const chatBtn = byId('aser-view-mode-chat-btn');
|
|
1201
|
+
const graphBtn = byId('aser-view-mode-graph-btn');
|
|
1202
|
+
const threadBtn = byId('aser-view-mode-thread-btn');
|
|
1203
|
+
const chatPanel = byId('aser-chat-process-container');
|
|
1204
|
+
const graphPanel = byId('aser-graph-process-container');
|
|
1205
|
+
const threadPanel = byId('aser-thread-process-container');
|
|
1206
|
+
const threadFilterSelect = byId('aser-thread-filter-select');
|
|
1207
|
+
if (chatBtn instanceof HTMLButtonElement) chatBtn.classList.toggle('active', state.viewMode === 'chat');
|
|
1208
|
+
if (graphBtn instanceof HTMLButtonElement) graphBtn.classList.toggle('active', state.viewMode === 'graph');
|
|
1209
|
+
if (threadBtn instanceof HTMLButtonElement) threadBtn.classList.toggle('active', state.viewMode === 'thread');
|
|
1210
|
+
if (chatPanel instanceof HTMLElement) chatPanel.classList.toggle('hidden', state.viewMode !== 'chat');
|
|
1211
|
+
if (graphPanel instanceof HTMLElement) graphPanel.classList.toggle('hidden', state.viewMode !== 'graph');
|
|
1212
|
+
if (threadPanel instanceof HTMLElement) threadPanel.classList.toggle('hidden', state.viewMode !== 'thread');
|
|
1213
|
+
if (threadFilterSelect instanceof HTMLSelectElement) threadFilterSelect.value = state.threadFilter;
|
|
1214
|
+
if (state.viewMode === 'chat') renderChatProcess();
|
|
1215
|
+
if (state.viewMode === 'graph') renderTaskGraph();
|
|
1216
|
+
if (state.viewMode === 'thread') renderTaskThread();
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function renderFocusDetail() {
|
|
1220
|
+
const detail = byId('aser-focus-detail');
|
|
1221
|
+
if (!(detail instanceof HTMLElement)) return;
|
|
1222
|
+
if (!state.selectedProject) {
|
|
1223
|
+
detail.textContent = '请先选择 Project。';
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (!state.focusedEntity?.taskId) {
|
|
1227
|
+
detail.textContent = '点击中间区域中的 chat 任务或图形 task,查看详细信息。';
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const task = findTaskById(state.focusedEntity.taskId);
|
|
1231
|
+
if (!task) {
|
|
1232
|
+
detail.textContent = '选中对象不存在或已被更新。';
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
const artifacts = (state.selectedProject?.artifacts || []).filter((artifact) => artifact.task_id === task.id);
|
|
1236
|
+
const evaluations = (state.selectedProject?.evaluations || []).filter((row) => row.task_id === task.id);
|
|
1237
|
+
const children = getTasks().filter((row) => String(row.parent || '') === String(task.id));
|
|
1238
|
+
const related = Array.isArray(task.related) ? task.related : [];
|
|
1239
|
+
const lastEvaluation = evaluations
|
|
1240
|
+
.slice()
|
|
1241
|
+
.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')))[0] || null;
|
|
1242
|
+
const intents = Array.isArray(state.selectedProject?.intents) ? state.selectedProject.intents : [];
|
|
1243
|
+
const intent = intents.find((row) => String(row.id) === String(task.intent_id));
|
|
1244
|
+
const sourceEvent = (state.selectedProject?.events || []).find((eventRow) => String(eventRow.id) === String(intent?.source_event_id || ''));
|
|
1245
|
+
const protocol = getActiveProtocol();
|
|
1246
|
+
const gates = Array.isArray(protocol?.quality_gates) ? protocol.quality_gates : [];
|
|
1247
|
+
const gate = gates.find((row) => String(row.stage || '') === String(intent?.taxonomy || ''));
|
|
1248
|
+
const expectedEvidence = Array.isArray(gate?.required_evidence) ? gate.required_evidence : [];
|
|
1249
|
+
const providedEvidenceTypes = artifacts.map((row) => String(row.type || '').trim()).filter(Boolean);
|
|
1250
|
+
const missingEvidence = expectedEvidence.filter((type) => !providedEvidenceTypes.includes(String(type)));
|
|
1251
|
+
const evidenceSummary = expectedEvidence.length
|
|
1252
|
+
? `gate ${safeText(String(gate?.gate_id || '-'))}: 需要 ${safeText(expectedEvidence.join(', '))}`
|
|
1253
|
+
: '当前阶段未配置闸门证据要求';
|
|
1254
|
+
const acceptBlockedByGate = expectedEvidence.length > 0 && missingEvidence.length > 0;
|
|
1255
|
+
const stage = PM_STAGE_LABELS[String(intent?.taxonomy || '')] || String(intent?.taxonomy || 'unknown');
|
|
1256
|
+
const replayButton = isReplayEligibleStatus(task.status)
|
|
1257
|
+
? `<button class="btn btn-secondary btn-sm" data-focus-action="replay-task" data-focus-task-id="${safeText(task.id)}" type="button">重访任务</button>`
|
|
1258
|
+
: '';
|
|
1259
|
+
const evidenceGapBlock = `
|
|
1260
|
+
<section class="aser-focus-section">
|
|
1261
|
+
<div class="aser-focus-section-title">证据缺口</div>
|
|
1262
|
+
<div class="aser-focus-line">${evidenceSummary}</div>
|
|
1263
|
+
<div class="aser-focus-line">已提交:${safeText(providedEvidenceTypes.join(', ') || '-')}</div>
|
|
1264
|
+
<div class="aser-evidence-gap-row">
|
|
1265
|
+
${missingEvidence.length
|
|
1266
|
+
? missingEvidence.map((item) => `<span class="aser-evidence-chip missing">${safeText(item)}</span>`).join('')
|
|
1267
|
+
: '<span class="aser-evidence-chip done">证据齐全</span>'}
|
|
1268
|
+
</div>
|
|
1269
|
+
</section>
|
|
1270
|
+
`;
|
|
1271
|
+
const focusSections = `
|
|
1272
|
+
<section class="aser-focus-section">
|
|
1273
|
+
<div class="aser-focus-section-title">上下文</div>
|
|
1274
|
+
<div class="aser-focus-line">source: ${safeText(state.focusedEntity.source || '-')}</div>
|
|
1275
|
+
<div class="aser-focus-line">stage: ${safeText(stage)}</div>
|
|
1276
|
+
<div class="aser-focus-line">status: ${safeText(task.status || '-')}</div>
|
|
1277
|
+
<div class="aser-focus-line">owner: ${safeText(task.owner_role || '-')} | agent: ${safeText(task.preferred_agent || '-')}</div>
|
|
1278
|
+
<div class="aser-focus-line">thread: parent ${safeText(task.parent || 'none')} | children ${safeText(String(children.length))} | related ${safeText(String(related.length))}</div>
|
|
1279
|
+
</section>
|
|
1280
|
+
<section class="aser-focus-section">
|
|
1281
|
+
<div class="aser-focus-section-title">执行证据</div>
|
|
1282
|
+
<div class="aser-focus-line">acceptance: ${safeText(task.acceptance_criteria || '-')}</div>
|
|
1283
|
+
<div class="aser-focus-line">evaluation_policy: ${safeText(task.evaluation_policy || '-')}</div>
|
|
1284
|
+
<div class="aser-focus-line">artifacts: ${safeText(String(artifacts.length))}</div>
|
|
1285
|
+
<div class="aser-focus-line">evaluations: ${safeText(String(evaluations.length))}</div>
|
|
1286
|
+
<div class="aser-focus-line">source_event: ${safeText(sourceEvent?.type || '-')} | at ${safeText(formatDateTime(sourceEvent?.timestamp || sourceEvent?.created_at))}</div>
|
|
1287
|
+
<div class="aser-focus-line">last_evaluation: ${safeText(lastEvaluation ? `${lastEvaluation.decision} by ${lastEvaluation.evaluator}` : '-')}</div>
|
|
1288
|
+
</section>
|
|
1289
|
+
${evidenceGapBlock}
|
|
1290
|
+
`;
|
|
1291
|
+
detail.innerHTML = `
|
|
1292
|
+
<div class="aser-focus-title">${safeText(task.title || task.id)}</div>
|
|
1293
|
+
${focusSections}
|
|
1294
|
+
<div class="aser-inline-actions">
|
|
1295
|
+
${replayButton}
|
|
1296
|
+
<button class="btn btn-secondary btn-sm" data-focus-action="loop-clarify" data-focus-task-id="${safeText(task.id)}" type="button">发起澄清回路</button>
|
|
1297
|
+
<button class="btn btn-secondary btn-sm" data-focus-action="loop-escalate" data-focus-task-id="${safeText(task.id)}" type="button">发起上升回路</button>
|
|
1298
|
+
<button class="btn btn-secondary btn-sm" data-focus-action="add-artifact" data-focus-task-id="${safeText(task.id)}" type="button">添加产物</button>
|
|
1299
|
+
<button class="btn btn-secondary btn-sm" data-focus-action="eval-accept" data-focus-task-id="${safeText(task.id)}" type="button" ${acceptBlockedByGate ? 'disabled' : ''} title="${acceptBlockedByGate ? '缺少闸门证据,无法直接通过' : ''}">评价通过</button>
|
|
1300
|
+
<button class="btn btn-secondary btn-sm" data-focus-action="eval-revise" data-focus-task-id="${safeText(task.id)}" type="button">评价修订</button>
|
|
1301
|
+
</div>
|
|
1302
|
+
`;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function renderAll() {
|
|
1306
|
+
renderControls();
|
|
1307
|
+
renderSummary();
|
|
1308
|
+
renderProtocolPresetSelectors();
|
|
1309
|
+
renderDemoPanel();
|
|
1310
|
+
renderProjectList();
|
|
1311
|
+
renderProjectSummary();
|
|
1312
|
+
renderProtocolVisual();
|
|
1313
|
+
renderPmBoard();
|
|
1314
|
+
renderThreadSummary();
|
|
1315
|
+
renderGovernanceAdvice();
|
|
1316
|
+
renderModePanels();
|
|
1317
|
+
renderFocusDetail();
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
async function deleteProject(projectId) {
|
|
1321
|
+
if (state.readonly || !state.hasAccess || !projectId) return;
|
|
1322
|
+
const hit = state.projects.find((row) => String(row.id) === String(projectId));
|
|
1323
|
+
const ok = window.confirm(`确认删除 Project「${hit?.name || projectId}」?\n该操作不可恢复。`);
|
|
1324
|
+
if (!ok) return;
|
|
1325
|
+
await apiRequest(`${API_BASE}/aser-runtime/projects/${projectId}`, { method: 'DELETE' });
|
|
1326
|
+
if (String(state.selectedProjectId) === String(projectId)) {
|
|
1327
|
+
state.selectedProjectId = '';
|
|
1328
|
+
state.focusedEntity = null;
|
|
1329
|
+
}
|
|
1330
|
+
if (typeof options.assignProjectTeam === 'function') {
|
|
1331
|
+
options.assignProjectTeam(projectId, '');
|
|
1332
|
+
}
|
|
1333
|
+
await loadProjects();
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
async function loadProjectDetail(projectId) {
|
|
1337
|
+
if (!projectId) {
|
|
1338
|
+
state.selectedProject = null;
|
|
1339
|
+
state.selectedProjectState = null;
|
|
1340
|
+
state.selectedProtocol = null;
|
|
1341
|
+
state.focusedEntity = null;
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
const [project, projectState] = await Promise.all([
|
|
1345
|
+
apiRequest(`${API_BASE}/aser-runtime/projects/${projectId}`),
|
|
1346
|
+
apiRequest(`${API_BASE}/aser-runtime/projects/${projectId}/state`),
|
|
1347
|
+
]);
|
|
1348
|
+
state.selectedProject = project || null;
|
|
1349
|
+
state.selectedProjectState = projectState || null;
|
|
1350
|
+
state.pulledTask = null;
|
|
1351
|
+
state.focusedEntity = null;
|
|
1352
|
+
refreshSelectedRunTeam();
|
|
1353
|
+
try {
|
|
1354
|
+
const protocol = await apiRequest(`${API_BASE}/aser-runtime/projects/${projectId}/protocol`);
|
|
1355
|
+
state.selectedProtocol = protocol || null;
|
|
1356
|
+
if (protocol?.id) {
|
|
1357
|
+
const existed = state.protocolPresets.some((row) => String(row.id) === String(protocol.id));
|
|
1358
|
+
if (!existed) state.protocolPresets.push(protocol);
|
|
1359
|
+
state.selectedProtocolPresetId = String(protocol.id);
|
|
1360
|
+
}
|
|
1361
|
+
} catch {
|
|
1362
|
+
state.selectedProtocol = null;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
async function loadProjects() {
|
|
1367
|
+
if (!state.hasAccess) {
|
|
1368
|
+
state.projects = [];
|
|
1369
|
+
state.selectedProjectId = '';
|
|
1370
|
+
state.selectedProject = null;
|
|
1371
|
+
state.selectedProjectState = null;
|
|
1372
|
+
state.focusedEntity = null;
|
|
1373
|
+
renderAll();
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
const projects = await apiRequest(`${API_BASE}/aser-runtime/projects`);
|
|
1377
|
+
state.projects = Array.isArray(projects) ? projects : [];
|
|
1378
|
+
if (!state.selectedProjectId && state.projects.length) {
|
|
1379
|
+
state.selectedProjectId = String(state.projects[0].id || '');
|
|
1380
|
+
}
|
|
1381
|
+
if (state.selectedProjectId && !state.projects.some((project) => String(project.id) === String(state.selectedProjectId))) {
|
|
1382
|
+
state.selectedProjectId = state.projects[0] ? String(state.projects[0].id || '') : '';
|
|
1383
|
+
}
|
|
1384
|
+
refreshSelectedRunTeam();
|
|
1385
|
+
await loadProjectDetail(state.selectedProjectId);
|
|
1386
|
+
renderProtocolPresetSelectors();
|
|
1387
|
+
renderAll();
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
async function createProject() {
|
|
1391
|
+
if (state.readonly || !state.hasAccess) return;
|
|
1392
|
+
const nameInput = byId('aser-project-name-input');
|
|
1393
|
+
const descInput = byId('aser-project-description-input');
|
|
1394
|
+
const protocolSelect = byId('aser-create-project-protocol-select');
|
|
1395
|
+
if (!(nameInput instanceof HTMLInputElement)
|
|
1396
|
+
|| !(descInput instanceof HTMLTextAreaElement)
|
|
1397
|
+
|| !(protocolSelect instanceof HTMLSelectElement)) {
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
const name = nameInput.value.trim();
|
|
1401
|
+
if (!name) {
|
|
1402
|
+
alert('请输入项目名称');
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const description = descInput.value.trim();
|
|
1406
|
+
const protocolPresetId = String(protocolSelect.value || state.selectedProtocolPresetId || DEFAULT_PROTOCOL_PRESET.id);
|
|
1407
|
+
const created = await apiRequest(`${API_BASE}/aser-runtime/projects`, {
|
|
1408
|
+
method: 'POST',
|
|
1409
|
+
body: JSON.stringify({ name, description }),
|
|
1410
|
+
});
|
|
1411
|
+
const selectedPreset = state.protocolPresets.find((row) => String(row.id) === protocolPresetId) || DEFAULT_PROTOCOL_PRESET;
|
|
1412
|
+
if (created?.id && selectedPreset) {
|
|
1413
|
+
await apiRequest(`${API_BASE}/aser-runtime/projects/${created.id}/protocol`, {
|
|
1414
|
+
method: 'PUT',
|
|
1415
|
+
body: JSON.stringify(selectedPreset),
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
nameInput.value = '';
|
|
1419
|
+
descInput.value = '';
|
|
1420
|
+
await loadProjects();
|
|
1421
|
+
if (created?.id) {
|
|
1422
|
+
state.selectedProjectId = String(created.id);
|
|
1423
|
+
await loadProjectDetail(state.selectedProjectId);
|
|
1424
|
+
renderAll();
|
|
1425
|
+
}
|
|
1426
|
+
closeDialog('aser-create-project-dialog');
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
async function injectComplexScenario() {
|
|
1430
|
+
if (state.readonly || !state.hasAccess) return;
|
|
1431
|
+
const selectedPreset = state.protocolPresets.find((row) => String(row.id) === String(state.selectedProtocolPresetId)) || DEFAULT_PROTOCOL_PRESET;
|
|
1432
|
+
const created = await apiRequest(`${API_BASE}/aser-runtime/projects`, {
|
|
1433
|
+
method: 'POST',
|
|
1434
|
+
body: JSON.stringify({
|
|
1435
|
+
name: `Thread 波折场景 ${new Date().toLocaleTimeString()}`,
|
|
1436
|
+
description: '模拟多轮反复:澄清、返工、阻塞、上升、恢复、验收',
|
|
1437
|
+
}),
|
|
1438
|
+
});
|
|
1439
|
+
const projectId = String(created?.id || '');
|
|
1440
|
+
if (!projectId) return;
|
|
1441
|
+
await apiRequest(`${API_BASE}/aser-runtime/projects/${projectId}/protocol`, {
|
|
1442
|
+
method: 'PUT',
|
|
1443
|
+
body: JSON.stringify(selectedPreset),
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
const createByEvent = async (payload) => {
|
|
1447
|
+
const result = await apiRequest(`${API_BASE}/aser-runtime/projects/${projectId}/submit-event`, {
|
|
1448
|
+
method: 'POST',
|
|
1449
|
+
body: JSON.stringify(payload),
|
|
1450
|
+
});
|
|
1451
|
+
return String(result?.task?.id || '');
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
const submitResult = async (taskId, role, actor, input = {}) => {
|
|
1455
|
+
if (!taskId) return;
|
|
1456
|
+
await apiRequest(`${API_BASE}/aser-runtime/projects/${projectId}/tasks/${taskId}/submit-result`, {
|
|
1457
|
+
method: 'POST',
|
|
1458
|
+
body: JSON.stringify({
|
|
1459
|
+
role,
|
|
1460
|
+
actor,
|
|
1461
|
+
artifacts: Array.isArray(input.artifacts) ? input.artifacts : [],
|
|
1462
|
+
mark_blocked: Boolean(input.mark_blocked),
|
|
1463
|
+
blocked_reason: String(input.blocked_reason || ''),
|
|
1464
|
+
}),
|
|
1465
|
+
});
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
const evalTask = async (taskId, decision, evaluator, comments, score = 80) => {
|
|
1469
|
+
if (!taskId) return;
|
|
1470
|
+
await apiRequest(`${API_BASE}/aser-runtime/projects/${projectId}/tasks/${taskId}/evaluations`, {
|
|
1471
|
+
method: 'POST',
|
|
1472
|
+
body: JSON.stringify({
|
|
1473
|
+
evaluator,
|
|
1474
|
+
decision,
|
|
1475
|
+
comments,
|
|
1476
|
+
score,
|
|
1477
|
+
}),
|
|
1478
|
+
});
|
|
1479
|
+
};
|
|
1480
|
+
|
|
1481
|
+
const reqTask = await createByEvent({
|
|
1482
|
+
actor: 'PM Alice',
|
|
1483
|
+
actor_kind: 'human',
|
|
1484
|
+
actor_role: 'product_manager',
|
|
1485
|
+
type: 'requirement_defined',
|
|
1486
|
+
payload: { intent_title: '多渠道支付API需求', task_title: 'T1 需求定义' },
|
|
1487
|
+
});
|
|
1488
|
+
await submitResult(reqTask, 'product_manager', 'PM Alice', {
|
|
1489
|
+
artifacts: [{ type: 'requirement_doc', uri: 'mock://req/v1', summary: '支付成功率SLO与回调约束' }],
|
|
1490
|
+
});
|
|
1491
|
+
await evalTask(reqTask, 'accept', 'PM Alice', '需求通过', 92);
|
|
1492
|
+
|
|
1493
|
+
const designV1 = await createByEvent({
|
|
1494
|
+
actor: 'Architect Bob',
|
|
1495
|
+
actor_kind: 'human',
|
|
1496
|
+
actor_role: 'architect',
|
|
1497
|
+
type: 'design_defined',
|
|
1498
|
+
payload: { intent_title: '架构方案v1', task_title: 'T2 方案设计v1', parent: reqTask, related: [reqTask] },
|
|
1499
|
+
});
|
|
1500
|
+
await submitResult(designV1, 'architect', 'Architect Bob', {
|
|
1501
|
+
artifacts: [{ type: 'design_doc', uri: 'mock://design/v1', summary: '初版架构' }],
|
|
1502
|
+
});
|
|
1503
|
+
await evalTask(designV1, 'revise', 'Reviewer Carol', '幂等策略与重试窗口不充分', 65);
|
|
1504
|
+
|
|
1505
|
+
const clarify1 = await createByEvent({
|
|
1506
|
+
actor: 'DevAgent',
|
|
1507
|
+
actor_kind: 'agent',
|
|
1508
|
+
actor_role: 'developer',
|
|
1509
|
+
type: 'message_posted',
|
|
1510
|
+
payload: { intent_taxonomy: 'clarify_design', intent_title: '澄清重试与幂等', task_title: 'T3 澄清回路#1', parent: designV1, related: [designV1] },
|
|
1511
|
+
});
|
|
1512
|
+
await submitResult(clarify1, 'developer', 'DevAgent', {
|
|
1513
|
+
artifacts: [{ type: 'clarification_note', uri: 'mock://clarify/1', summary: '补充澄清说明' }],
|
|
1514
|
+
});
|
|
1515
|
+
await evalTask(clarify1, 'accept', 'Architect Bob', '澄清通过', 85);
|
|
1516
|
+
|
|
1517
|
+
const designV2 = await createByEvent({
|
|
1518
|
+
actor: 'Architect Bob',
|
|
1519
|
+
actor_kind: 'human',
|
|
1520
|
+
actor_role: 'architect',
|
|
1521
|
+
type: 'design_defined',
|
|
1522
|
+
payload: { intent_title: '架构方案v2', task_title: 'T4 方案设计v2', parent: clarify1, related: [designV1, clarify1] },
|
|
1523
|
+
});
|
|
1524
|
+
await submitResult(designV2, 'architect', 'Architect Bob', {
|
|
1525
|
+
artifacts: [{ type: 'design_doc', uri: 'mock://design/v2', summary: '修订后架构' }],
|
|
1526
|
+
});
|
|
1527
|
+
await evalTask(designV2, 'accept', 'Reviewer Carol', '设计通过', 90);
|
|
1528
|
+
|
|
1529
|
+
const implV1 = await createByEvent({
|
|
1530
|
+
actor: 'DevAgent',
|
|
1531
|
+
actor_kind: 'agent',
|
|
1532
|
+
actor_role: 'developer',
|
|
1533
|
+
type: 'implementation_reported',
|
|
1534
|
+
payload: { intent_title: '实现迭代1', task_title: 'T5 开发实现v1', parent: designV2, related: [designV2] },
|
|
1535
|
+
});
|
|
1536
|
+
await submitResult(implV1, 'developer', 'DevAgent', {
|
|
1537
|
+
artifacts: [{ type: 'pr', uri: 'mock://pr/101', summary: '实现v1' }],
|
|
1538
|
+
mark_blocked: true,
|
|
1539
|
+
blocked_reason: '第三方风控网关超时,联调阻塞',
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
const issue1 = await createByEvent({
|
|
1543
|
+
actor: 'DevAgent',
|
|
1544
|
+
actor_kind: 'agent',
|
|
1545
|
+
actor_role: 'developer',
|
|
1546
|
+
type: 'issue_reported',
|
|
1547
|
+
payload: { intent_taxonomy: 'report_issue', intent_title: '联调阻塞问题', task_title: 'T6 问题记录', parent: implV1, related: [implV1] },
|
|
1548
|
+
});
|
|
1549
|
+
await submitResult(issue1, 'developer', 'DevAgent');
|
|
1550
|
+
await evalTask(issue1, 'accept', 'Reviewer Carol', '问题确认', 84);
|
|
1551
|
+
|
|
1552
|
+
const escalate1 = await createByEvent({
|
|
1553
|
+
actor: 'Reviewer Carol',
|
|
1554
|
+
actor_kind: 'human',
|
|
1555
|
+
actor_role: 'reviewer',
|
|
1556
|
+
type: 'escalation_requested',
|
|
1557
|
+
payload: { intent_taxonomy: 'escalate_issue', intent_title: '上升风险到PM', task_title: 'T7 上升回路', parent: issue1, related: [issue1, implV1] },
|
|
1558
|
+
});
|
|
1559
|
+
await submitResult(escalate1, 'reviewer', 'Reviewer Carol', {
|
|
1560
|
+
artifacts: [{ type: 'risk_summary', uri: 'mock://risk/1', summary: '风险升级记录' }],
|
|
1561
|
+
});
|
|
1562
|
+
await evalTask(escalate1, 'accept', 'PM Alice', '上升已确认并分配资源', 91);
|
|
1563
|
+
|
|
1564
|
+
const implV2 = await createByEvent({
|
|
1565
|
+
actor: 'DevAgent',
|
|
1566
|
+
actor_kind: 'agent',
|
|
1567
|
+
actor_role: 'developer',
|
|
1568
|
+
type: 'implementation_reported',
|
|
1569
|
+
payload: { intent_title: '实现迭代2', task_title: 'T8 开发实现v2', parent: implV1, related: [implV1, escalate1] },
|
|
1570
|
+
});
|
|
1571
|
+
await submitResult(implV2, 'developer', 'DevAgent', {
|
|
1572
|
+
artifacts: [
|
|
1573
|
+
{ type: 'pr', uri: 'mock://pr/102', summary: '修复后实现' },
|
|
1574
|
+
{ type: 'test_report', uri: 'mock://report/102', summary: '回归测试通过' },
|
|
1575
|
+
],
|
|
1576
|
+
});
|
|
1577
|
+
await evalTask(implV2, 'accept', 'Reviewer Carol', '实现恢复通过', 94);
|
|
1578
|
+
|
|
1579
|
+
const review = await createByEvent({
|
|
1580
|
+
actor: 'Reviewer Carol',
|
|
1581
|
+
actor_kind: 'human',
|
|
1582
|
+
actor_role: 'reviewer',
|
|
1583
|
+
type: 'review_requested',
|
|
1584
|
+
payload: { intent_title: '最终代码评审', task_title: 'T9 最终评审', parent: implV2, related: [implV2] },
|
|
1585
|
+
});
|
|
1586
|
+
await submitResult(review, 'reviewer', 'Reviewer Carol', {
|
|
1587
|
+
artifacts: [{ type: 'review_record', uri: 'mock://review/9', summary: '最终评审记录' }],
|
|
1588
|
+
});
|
|
1589
|
+
await evalTask(review, 'accept', 'Reviewer Carol', '最终验收通过', 93);
|
|
1590
|
+
|
|
1591
|
+
await loadProjects();
|
|
1592
|
+
state.selectedProjectId = projectId;
|
|
1593
|
+
state.viewMode = 'thread';
|
|
1594
|
+
await loadProjectDetail(projectId);
|
|
1595
|
+
renderAll();
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function buildDemoSteps(targetTaskId) {
|
|
1599
|
+
const task = findTaskById(targetTaskId);
|
|
1600
|
+
if (!task) return [];
|
|
1601
|
+
const intent = (state.selectedProject?.intents || []).find((row) => String(row.id) === String(task.intent_id));
|
|
1602
|
+
const stage = PM_STAGE_LABELS[String(intent?.taxonomy || '')] || String(intent?.taxonomy || 'unknown');
|
|
1603
|
+
const artifacts = (state.selectedProject?.artifacts || []).filter((row) => row.task_id === task.id);
|
|
1604
|
+
const evaluations = (state.selectedProject?.evaluations || []).filter((row) => row.task_id === task.id);
|
|
1605
|
+
const steps = [
|
|
1606
|
+
{ label: `Step 1: 创建任务 (${stage})`, taskId: String(task.id) },
|
|
1607
|
+
];
|
|
1608
|
+
if (artifacts.length) steps.push({ label: `Step ${steps.length + 1}: 提交执行产物 x${artifacts.length}`, taskId: String(task.id) });
|
|
1609
|
+
if (evaluations.length) steps.push({ label: `Step ${steps.length + 1}: 进入评审 x${evaluations.length}`, taskId: String(task.id) });
|
|
1610
|
+
steps.push({ label: `Step ${steps.length + 1}: 任务完成 (${task.status})`, taskId: String(task.id) });
|
|
1611
|
+
return steps;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
async function runDemoStep(stepIndex) {
|
|
1615
|
+
if (!state.demo.started || stepIndex < 0 || stepIndex >= state.demo.steps.length) return;
|
|
1616
|
+
if (state.demo.running) return;
|
|
1617
|
+
state.demo.running = true;
|
|
1618
|
+
const startAt = Date.now();
|
|
1619
|
+
try {
|
|
1620
|
+
const step = state.demo.steps[stepIndex];
|
|
1621
|
+
await sleep(280);
|
|
1622
|
+
state.demo.stepIndex = stepIndex;
|
|
1623
|
+
const focusTaskId = String(step.taskId || '');
|
|
1624
|
+
if (focusTaskId) setFocusedTask(String(focusTaskId), state.viewMode);
|
|
1625
|
+
const elapsed = Date.now() - startAt;
|
|
1626
|
+
state.demo.logs.push(`[${new Date().toLocaleTimeString()}] 回放: ${step.label} (${elapsed}ms)`);
|
|
1627
|
+
} finally {
|
|
1628
|
+
state.demo.running = false;
|
|
1629
|
+
renderAll();
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
async function runDemoNext() {
|
|
1634
|
+
const next = state.demo.stepIndex + 1;
|
|
1635
|
+
if (next >= state.demo.steps.length) return;
|
|
1636
|
+
await runDemoStep(next);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
async function runDemoAuto() {
|
|
1640
|
+
if (!state.demo.started || state.demo.running) return;
|
|
1641
|
+
state.demo.auto = true;
|
|
1642
|
+
renderAll();
|
|
1643
|
+
while (state.demo.auto && state.demo.stepIndex < state.demo.steps.length - 1) {
|
|
1644
|
+
// Controlled interval to let UI render each phase.
|
|
1645
|
+
await runDemoNext();
|
|
1646
|
+
await sleep(700);
|
|
1647
|
+
}
|
|
1648
|
+
state.demo.auto = false;
|
|
1649
|
+
renderAll();
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function resetDemo() {
|
|
1653
|
+
state.demo.auto = false;
|
|
1654
|
+
state.demo.running = false;
|
|
1655
|
+
state.demo.started = false;
|
|
1656
|
+
state.demo.stepIndex = -1;
|
|
1657
|
+
state.demo.projectId = '';
|
|
1658
|
+
state.demo.context = {};
|
|
1659
|
+
state.demo.logs = [];
|
|
1660
|
+
state.demo.steps = [];
|
|
1661
|
+
renderAll();
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function stopDemoAuto() {
|
|
1665
|
+
state.demo.auto = false;
|
|
1666
|
+
renderAll();
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function startDemo(targetTaskId) {
|
|
1670
|
+
if (!state.selectedProjectId || !state.selectedProject) {
|
|
1671
|
+
alert('请先选择一个项目');
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const focusTask = targetTaskId ? findTaskById(targetTaskId) : getFocusedTask();
|
|
1675
|
+
if (!focusTask || !isReplayEligibleStatus(focusTask.status)) {
|
|
1676
|
+
alert('请先选择一个已完成任务再重放');
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
state.demo.started = true;
|
|
1680
|
+
state.demo.stepIndex = -1;
|
|
1681
|
+
state.demo.projectId = state.selectedProjectId;
|
|
1682
|
+
state.demo.context = {};
|
|
1683
|
+
state.demo.logs = [];
|
|
1684
|
+
state.demo.steps = buildDemoSteps(String(focusTask.id));
|
|
1685
|
+
if (!state.demo.steps.length) {
|
|
1686
|
+
state.demo.logs.push(`[${new Date().toLocaleTimeString()}] 该任务暂无可重放节点。`);
|
|
1687
|
+
} else {
|
|
1688
|
+
state.demo.logs.push(`[${new Date().toLocaleTimeString()}] 已初始化任务重放:${focusTask.title || focusTask.id},共 ${state.demo.steps.length} 步`);
|
|
1689
|
+
}
|
|
1690
|
+
renderAll();
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function assignTeamToSelectedProject() {
|
|
1694
|
+
if (state.readonly || !state.hasAccess || !state.selectedProjectId) return;
|
|
1695
|
+
const teamId = String(state.selectedRunTeamId || '').trim();
|
|
1696
|
+
if (!teamId) {
|
|
1697
|
+
alert('请先选择 Team');
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
if (typeof options.assignProjectTeam === 'function') {
|
|
1701
|
+
options.assignProjectTeam(state.selectedProjectId, teamId);
|
|
1702
|
+
}
|
|
1703
|
+
const team = getTeamById(teamId);
|
|
1704
|
+
if (team) {
|
|
1705
|
+
alert(`已将项目分派给 Team:${team.name}`);
|
|
1706
|
+
}
|
|
1707
|
+
renderAll();
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
async function startRunWithSelectedTeam() {
|
|
1711
|
+
if (state.readonly || !state.hasAccess || !state.selectedProjectId) return;
|
|
1712
|
+
const teamId = String(state.selectedRunTeamId || '').trim();
|
|
1713
|
+
if (!teamId) {
|
|
1714
|
+
alert('请先选择 Team');
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
const team = getTeamById(teamId);
|
|
1718
|
+
if (!team) {
|
|
1719
|
+
alert('所选 Team 不存在,请重新选择');
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
if (typeof options.assignProjectTeam === 'function') {
|
|
1723
|
+
options.assignProjectTeam(state.selectedProjectId, teamId);
|
|
1724
|
+
}
|
|
1725
|
+
await apiRequest(`${API_BASE}/aser-runtime/projects/${state.selectedProjectId}/submit-event`, {
|
|
1726
|
+
method: 'POST',
|
|
1727
|
+
body: JSON.stringify({
|
|
1728
|
+
actor: 'Team Dispatcher',
|
|
1729
|
+
actor_kind: 'human',
|
|
1730
|
+
actor_role: 'product_manager',
|
|
1731
|
+
type: 'requirement_defined',
|
|
1732
|
+
payload: {
|
|
1733
|
+
intent_taxonomy: 'create_requirement',
|
|
1734
|
+
intent_title: `Team dispatch: ${team.name}`,
|
|
1735
|
+
task_title: `启动执行:${team.name}`,
|
|
1736
|
+
source: 'manual_team_selection',
|
|
1737
|
+
team_id: teamId,
|
|
1738
|
+
team_name: team.name,
|
|
1739
|
+
team_agent_ids: Array.isArray(team.agent_ids) ? team.agent_ids : [],
|
|
1740
|
+
},
|
|
1741
|
+
}),
|
|
1742
|
+
});
|
|
1743
|
+
await loadProjects();
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
async function submitEventDriven() {
|
|
1747
|
+
if (state.readonly || !state.hasAccess || !state.selectedProjectId) return;
|
|
1748
|
+
const systemInput = byId('aser-system-input');
|
|
1749
|
+
if (!(systemInput instanceof HTMLTextAreaElement)) {
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
const text = systemInput.value.trim();
|
|
1753
|
+
if (!text) {
|
|
1754
|
+
alert('请输入系统指令');
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
const classified = classifySystemInput(text);
|
|
1758
|
+
if (!classified) return;
|
|
1759
|
+
const result = await apiRequest(`${API_BASE}/aser-runtime/projects/${state.selectedProjectId}/submit-event`, {
|
|
1760
|
+
method: 'POST',
|
|
1761
|
+
body: JSON.stringify({
|
|
1762
|
+
actor: classified.actor,
|
|
1763
|
+
actor_kind: classified.actorKind,
|
|
1764
|
+
actor_role: classified.role,
|
|
1765
|
+
type: classified.type,
|
|
1766
|
+
payload: {
|
|
1767
|
+
user_input: text,
|
|
1768
|
+
raw_input: text,
|
|
1769
|
+
intent_taxonomy: classified.taxonomy,
|
|
1770
|
+
intent_title: text,
|
|
1771
|
+
task_title: text,
|
|
1772
|
+
source: 'system_input',
|
|
1773
|
+
},
|
|
1774
|
+
}),
|
|
1775
|
+
});
|
|
1776
|
+
const violations = Array.isArray(result?.violations) ? result.violations : [];
|
|
1777
|
+
if (violations.length) {
|
|
1778
|
+
alert(`协议校验未通过:${violations.join(', ')}`);
|
|
1779
|
+
}
|
|
1780
|
+
systemInput.value = '';
|
|
1781
|
+
await loadProjects();
|
|
1782
|
+
if (result?.task?.id) setFocusedTask(String(result.task.id), 'chat');
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
async function submitLoopActionFromTask(taskId, kind) {
|
|
1786
|
+
if (state.readonly || !state.hasAccess || !state.selectedProjectId) return;
|
|
1787
|
+
const baseTask = findTaskById(taskId);
|
|
1788
|
+
if (!baseTask) return;
|
|
1789
|
+
const isEscalate = kind === 'escalate';
|
|
1790
|
+
const title = window.prompt(
|
|
1791
|
+
isEscalate
|
|
1792
|
+
? '输入上升原因(例如:跨团队依赖阻塞)'
|
|
1793
|
+
: '输入澄清问题(例如:支付回调重试策略)',
|
|
1794
|
+
'',
|
|
1795
|
+
) || '';
|
|
1796
|
+
if (!title.trim()) return;
|
|
1797
|
+
const baseRole = String(baseTask.owner_role || 'developer');
|
|
1798
|
+
const role = isEscalate ? 'reviewer' : baseRole;
|
|
1799
|
+
const actor = role === 'developer'
|
|
1800
|
+
? 'DevAgent'
|
|
1801
|
+
: role === 'architect'
|
|
1802
|
+
? 'Architect Bob'
|
|
1803
|
+
: role === 'product_manager'
|
|
1804
|
+
? 'PM Alice'
|
|
1805
|
+
: 'Reviewer Carol';
|
|
1806
|
+
const actorKind = role === 'developer' ? 'agent' : 'human';
|
|
1807
|
+
const related = [...new Set([String(baseTask.id), ...(Array.isArray(baseTask.related) ? baseTask.related : [])])];
|
|
1808
|
+
const result = await apiRequest(`${API_BASE}/aser-runtime/projects/${state.selectedProjectId}/submit-event`, {
|
|
1809
|
+
method: 'POST',
|
|
1810
|
+
body: JSON.stringify({
|
|
1811
|
+
actor,
|
|
1812
|
+
actor_kind: actorKind,
|
|
1813
|
+
actor_role: role,
|
|
1814
|
+
type: isEscalate ? 'escalation_requested' : 'message_posted',
|
|
1815
|
+
payload: {
|
|
1816
|
+
intent_title: isEscalate ? 'Escalate delivery risk' : 'Clarify design details',
|
|
1817
|
+
task_title: isEscalate ? `上升回路: ${title}` : `澄清回路: ${title}`,
|
|
1818
|
+
intent_taxonomy: isEscalate ? 'escalate_issue' : 'clarify_design',
|
|
1819
|
+
note: title,
|
|
1820
|
+
parent: String(baseTask.id),
|
|
1821
|
+
related,
|
|
1822
|
+
},
|
|
1823
|
+
}),
|
|
1824
|
+
});
|
|
1825
|
+
await loadProjects();
|
|
1826
|
+
if (result?.task?.id) setFocusedTask(String(result.task.id), state.viewMode);
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
async function loadProtocol() {
|
|
1830
|
+
if (!state.selectedProjectId) return;
|
|
1831
|
+
const protocol = await apiRequest(`${API_BASE}/aser-runtime/projects/${state.selectedProjectId}/protocol`);
|
|
1832
|
+
state.selectedProtocol = protocol || null;
|
|
1833
|
+
renderAll();
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
async function saveProtocol() {
|
|
1837
|
+
if (state.readonly || !state.hasAccess || !state.selectedProjectId) return;
|
|
1838
|
+
const protocolInput = byId('aser-protocol-json-input');
|
|
1839
|
+
if (!(protocolInput instanceof HTMLTextAreaElement)) return;
|
|
1840
|
+
let payload = {};
|
|
1841
|
+
try {
|
|
1842
|
+
payload = parseOptionalJsonText(protocolInput.value);
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
alert(error?.message || error);
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
await apiRequest(`${API_BASE}/aser-runtime/projects/${state.selectedProjectId}/protocol`, {
|
|
1848
|
+
method: 'PUT',
|
|
1849
|
+
body: JSON.stringify(payload),
|
|
1850
|
+
});
|
|
1851
|
+
await loadProjects();
|
|
1852
|
+
if (state.selectedProtocol?.id) state.selectedProtocolPresetId = String(state.selectedProtocol.id);
|
|
1853
|
+
closeDialog('aser-protocol-dialog');
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
async function pullTaskByRole() {
|
|
1857
|
+
if (state.readonly || !state.hasAccess || !state.selectedProjectId) return;
|
|
1858
|
+
const roleSelect = byId('aser-pull-role-select');
|
|
1859
|
+
if (!(roleSelect instanceof HTMLSelectElement)) return;
|
|
1860
|
+
const role = String(roleSelect.value || '').trim();
|
|
1861
|
+
if (!role) return;
|
|
1862
|
+
const pulled = await apiRequest(`${API_BASE}/aser-runtime/projects/${state.selectedProjectId}/pull-task?role=${encodeURIComponent(role)}`);
|
|
1863
|
+
state.pulledTask = pulled || null;
|
|
1864
|
+
if (state.pulledTask?.task?.id) {
|
|
1865
|
+
setFocusedTask(String(state.pulledTask.task.id), 'chat');
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
renderAll();
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
async function submitPulledTaskResult() {
|
|
1872
|
+
if (!state.pulledTask?.task?.id || !state.selectedProjectId) return;
|
|
1873
|
+
const taskId = state.pulledTask.task.id;
|
|
1874
|
+
const role = state.pulledTask.task.owner_role || 'developer';
|
|
1875
|
+
const uri = window.prompt('提交执行结果 artifact.uri(可空)', '') || '';
|
|
1876
|
+
const artifacts = uri
|
|
1877
|
+
? [{ type: 'execution_report', uri, summary: 'submitted from pull panel' }]
|
|
1878
|
+
: [];
|
|
1879
|
+
await apiRequest(`${API_BASE}/aser-runtime/projects/${state.selectedProjectId}/tasks/${taskId}/submit-result`, {
|
|
1880
|
+
method: 'POST',
|
|
1881
|
+
body: JSON.stringify({
|
|
1882
|
+
role,
|
|
1883
|
+
actor: role,
|
|
1884
|
+
artifacts,
|
|
1885
|
+
}),
|
|
1886
|
+
});
|
|
1887
|
+
await loadProjects();
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
async function addTaskArtifact(taskId) {
|
|
1891
|
+
const type = window.prompt('artifact.type(例如 test_report)', 'test_report');
|
|
1892
|
+
if (!type) return;
|
|
1893
|
+
const uri = window.prompt('artifact.uri(例如 https://ci/report/123)', '');
|
|
1894
|
+
if (!uri) return;
|
|
1895
|
+
const summary = window.prompt('artifact.summary(可选)', '') || '';
|
|
1896
|
+
await apiRequest(`${API_BASE}/aser-runtime/projects/${state.selectedProjectId}/tasks/${taskId}/artifacts`, {
|
|
1897
|
+
method: 'POST',
|
|
1898
|
+
body: JSON.stringify({ type, uri, summary }),
|
|
1899
|
+
});
|
|
1900
|
+
await loadProjects();
|
|
1901
|
+
setFocusedTask(taskId, state.viewMode);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
async function evaluateTask(taskId, decision) {
|
|
1905
|
+
const evaluator = window.prompt('evaluator(例如 QA Lead)', 'QA Lead');
|
|
1906
|
+
if (!evaluator) return;
|
|
1907
|
+
const scoreRaw = window.prompt('score(0-100,可选)', '80') || '0';
|
|
1908
|
+
const comments = window.prompt('comments(可选)', '') || '';
|
|
1909
|
+
const result = await apiRequest(`${API_BASE}/aser-runtime/projects/${state.selectedProjectId}/tasks/${taskId}/evaluations`, {
|
|
1910
|
+
method: 'POST',
|
|
1911
|
+
body: JSON.stringify({
|
|
1912
|
+
evaluator,
|
|
1913
|
+
decision,
|
|
1914
|
+
score: Number(scoreRaw || 0),
|
|
1915
|
+
comments,
|
|
1916
|
+
}),
|
|
1917
|
+
});
|
|
1918
|
+
if (decision === 'accept' && String(result?.decision || '') !== 'accept') {
|
|
1919
|
+
alert(`质量闸门拦截:本次评价已自动降级为 ${String(result?.decision || 'revise')},请先补齐证据。`);
|
|
1920
|
+
}
|
|
1921
|
+
await loadProjects();
|
|
1922
|
+
setFocusedTask(taskId, state.viewMode);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function bindEvents() {
|
|
1926
|
+
byId('aser-open-create-project-dialog-btn')?.addEventListener('click', () => {
|
|
1927
|
+
renderProtocolPresetSelectors();
|
|
1928
|
+
openDialog('aser-create-project-dialog');
|
|
1929
|
+
});
|
|
1930
|
+
byId('aser-open-protocol-dialog-btn')?.addEventListener('click', async () => {
|
|
1931
|
+
try {
|
|
1932
|
+
await loadProtocol();
|
|
1933
|
+
openDialog('aser-protocol-dialog');
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
alert(`加载 Protocol 失败: ${error?.message || error}`);
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
byId('aser-cancel-create-project-dialog-btn')?.addEventListener('click', () => closeDialog('aser-create-project-dialog'));
|
|
1939
|
+
byId('aser-cancel-protocol-dialog-btn')?.addEventListener('click', () => closeDialog('aser-protocol-dialog'));
|
|
1940
|
+
|
|
1941
|
+
byId('aser-create-project-btn')?.addEventListener('click', () => {
|
|
1942
|
+
createProject().catch((error) => alert(`创建 ASER 项目失败: ${error?.message || error}`));
|
|
1943
|
+
});
|
|
1944
|
+
byId('aser-load-complex-scenario-btn')?.addEventListener('click', () => {
|
|
1945
|
+
injectComplexScenario().catch((error) => alert(`注入复杂场景失败: ${error?.message || error}`));
|
|
1946
|
+
});
|
|
1947
|
+
byId('aser-protocol-preset-select')?.addEventListener('change', (event) => {
|
|
1948
|
+
const target = event.target;
|
|
1949
|
+
if (!(target instanceof HTMLSelectElement)) return;
|
|
1950
|
+
state.selectedProtocolPresetId = String(target.value || DEFAULT_PROTOCOL_PRESET.id);
|
|
1951
|
+
const next = state.projects.find((project) => String(project?.protocol?.id || '') === state.selectedProtocolPresetId);
|
|
1952
|
+
state.selectedProjectId = next ? String(next.id || '') : '';
|
|
1953
|
+
state.projectListPage = 1;
|
|
1954
|
+
loadProjectDetail(state.selectedProjectId).then(() => renderAll()).catch(() => renderAll());
|
|
1955
|
+
});
|
|
1956
|
+
byId('aser-create-project-protocol-select')?.addEventListener('change', (event) => {
|
|
1957
|
+
const target = event.target;
|
|
1958
|
+
if (!(target instanceof HTMLSelectElement)) return;
|
|
1959
|
+
state.selectedProtocolPresetId = String(target.value || DEFAULT_PROTOCOL_PRESET.id);
|
|
1960
|
+
renderAll();
|
|
1961
|
+
});
|
|
1962
|
+
|
|
1963
|
+
byId('aser-project-list')?.addEventListener('click', (event) => {
|
|
1964
|
+
const target = event.target;
|
|
1965
|
+
if (!(target instanceof HTMLElement)) return;
|
|
1966
|
+
const deleteBtn = target.closest('[data-aser-project-delete-id]');
|
|
1967
|
+
if (deleteBtn instanceof HTMLElement) {
|
|
1968
|
+
const projectId = String(deleteBtn.dataset.aserProjectDeleteId || '').trim();
|
|
1969
|
+
if (!projectId) return;
|
|
1970
|
+
deleteProject(projectId).catch((error) => alert(`删除 ASER 项目失败: ${error?.message || error}`));
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const openBtn = target.closest('[data-aser-project-open-id]');
|
|
1974
|
+
if (!(openBtn instanceof HTMLElement)) return;
|
|
1975
|
+
const projectId = String(openBtn.dataset.aserProjectOpenId || '').trim();
|
|
1976
|
+
if (!projectId) return;
|
|
1977
|
+
state.selectedProjectId = projectId;
|
|
1978
|
+
loadProjectDetail(projectId).then(() => renderAll()).catch((error) => alert(`加载 ASER 项目详情失败: ${error?.message || error}`));
|
|
1979
|
+
});
|
|
1980
|
+
byId('aser-project-list-pager')?.addEventListener('click', (event) => {
|
|
1981
|
+
const target = event.target;
|
|
1982
|
+
if (!(target instanceof HTMLElement)) return;
|
|
1983
|
+
const btn = target.closest('[data-aser-project-page-action]');
|
|
1984
|
+
if (!(btn instanceof HTMLElement)) return;
|
|
1985
|
+
const action = String(btn.dataset.aserProjectPageAction || '');
|
|
1986
|
+
if (action === 'prev') state.projectListPage = Math.max(1, Number(state.projectListPage || 1) - 1);
|
|
1987
|
+
if (action === 'next') state.projectListPage = Number(state.projectListPage || 1) + 1;
|
|
1988
|
+
renderProjectList();
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
byId('aser-submit-event-btn')?.addEventListener('click', () => {
|
|
1992
|
+
submitEventDriven().catch((error) => alert(`提交事件失败: ${error?.message || error}`));
|
|
1993
|
+
});
|
|
1994
|
+
byId('aser-run-team-select')?.addEventListener('change', (event) => {
|
|
1995
|
+
const target = event.target;
|
|
1996
|
+
if (!(target instanceof HTMLSelectElement)) return;
|
|
1997
|
+
state.selectedRunTeamId = String(target.value || '').trim();
|
|
1998
|
+
renderControls();
|
|
1999
|
+
});
|
|
2000
|
+
byId('aser-assign-team-btn')?.addEventListener('click', () => assignTeamToSelectedProject());
|
|
2001
|
+
byId('aser-start-run-btn')?.addEventListener('click', () => {
|
|
2002
|
+
startRunWithSelectedTeam().catch((error) => alert(`启动执行失败: ${error?.message || error}`));
|
|
2003
|
+
});
|
|
2004
|
+
byId('aser-demo-start-btn')?.addEventListener('click', () => startDemo());
|
|
2005
|
+
byId('aser-demo-next-btn')?.addEventListener('click', () => {
|
|
2006
|
+
runDemoNext().catch((error) => alert(`演示下一步失败: ${error?.message || error}`));
|
|
2007
|
+
});
|
|
2008
|
+
byId('aser-demo-auto-btn')?.addEventListener('click', () => {
|
|
2009
|
+
runDemoAuto().catch((error) => alert(`自动演示失败: ${error?.message || error}`));
|
|
2010
|
+
});
|
|
2011
|
+
byId('aser-demo-stop-btn')?.addEventListener('click', () => stopDemoAuto());
|
|
2012
|
+
byId('aser-demo-reset-btn')?.addEventListener('click', () => resetDemo());
|
|
2013
|
+
byId('aser-demo-steps')?.addEventListener('click', (event) => {
|
|
2014
|
+
const target = event.target;
|
|
2015
|
+
if (!(target instanceof HTMLElement)) return;
|
|
2016
|
+
const row = target.closest('[data-aser-demo-step-index]');
|
|
2017
|
+
if (!(row instanceof HTMLElement)) return;
|
|
2018
|
+
const stepIndex = Number(row.dataset.aserDemoStepIndex || -1);
|
|
2019
|
+
if (!Number.isInteger(stepIndex) || stepIndex < 0 || stepIndex > state.demo.stepIndex) return;
|
|
2020
|
+
state.demo.logs.push(`[${new Date().toLocaleTimeString()}] 回看: ${state.demo.steps[stepIndex]?.label || stepIndex}`);
|
|
2021
|
+
renderAll();
|
|
2022
|
+
});
|
|
2023
|
+
byId('aser-load-protocol-btn')?.addEventListener('click', () => {
|
|
2024
|
+
loadProtocol().catch((error) => alert(`加载 Protocol 失败: ${error?.message || error}`));
|
|
2025
|
+
});
|
|
2026
|
+
byId('aser-save-protocol-btn')?.addEventListener('click', () => {
|
|
2027
|
+
saveProtocol().catch((error) => alert(`保存 Protocol 失败: ${error?.message || error}`));
|
|
2028
|
+
});
|
|
2029
|
+
byId('aser-view-mode-chat-btn')?.addEventListener('click', () => {
|
|
2030
|
+
state.viewMode = 'chat';
|
|
2031
|
+
renderAll();
|
|
2032
|
+
});
|
|
2033
|
+
byId('aser-view-mode-graph-btn')?.addEventListener('click', () => {
|
|
2034
|
+
state.viewMode = 'graph';
|
|
2035
|
+
renderAll();
|
|
2036
|
+
});
|
|
2037
|
+
byId('aser-view-mode-thread-btn')?.addEventListener('click', () => {
|
|
2038
|
+
state.viewMode = 'thread';
|
|
2039
|
+
renderAll();
|
|
2040
|
+
});
|
|
2041
|
+
byId('aser-thread-filter-select')?.addEventListener('change', (event) => {
|
|
2042
|
+
const target = event.target;
|
|
2043
|
+
if (!(target instanceof HTMLSelectElement)) return;
|
|
2044
|
+
state.threadFilter = String(target.value || 'all');
|
|
2045
|
+
renderAll();
|
|
2046
|
+
});
|
|
2047
|
+
byId('aser-protocol-view-graph-btn')?.addEventListener('click', () => {
|
|
2048
|
+
state.protocolViewMode = 'graph';
|
|
2049
|
+
renderAll();
|
|
2050
|
+
});
|
|
2051
|
+
byId('aser-protocol-view-text-btn')?.addEventListener('click', () => {
|
|
2052
|
+
state.protocolViewMode = 'text';
|
|
2053
|
+
renderAll();
|
|
2054
|
+
});
|
|
2055
|
+
byId('aser-protocol-expand-btn')?.addEventListener('click', () => {
|
|
2056
|
+
renderProtocolVisual();
|
|
2057
|
+
openDialog('aser-protocol-flow-dialog');
|
|
2058
|
+
});
|
|
2059
|
+
byId('aser-close-protocol-flow-dialog-btn')?.addEventListener('click', () => closeDialog('aser-protocol-flow-dialog'));
|
|
2060
|
+
byId('aser-protocol-coverage-toggle')?.addEventListener('change', (event) => {
|
|
2061
|
+
const target = event.target;
|
|
2062
|
+
if (!(target instanceof HTMLInputElement)) return;
|
|
2063
|
+
state.protocolCoverageEnabled = Boolean(target.checked);
|
|
2064
|
+
renderAll();
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
byId('aser-chat-process-list')?.addEventListener('click', (event) => {
|
|
2068
|
+
const target = event.target;
|
|
2069
|
+
if (!(target instanceof HTMLElement)) return;
|
|
2070
|
+
const card = target.closest('[data-aser-chat-task-id]');
|
|
2071
|
+
if (!(card instanceof HTMLElement)) return;
|
|
2072
|
+
const taskId = String(card.dataset.aserChatTaskId || '').trim();
|
|
2073
|
+
if (!taskId) return;
|
|
2074
|
+
setFocusedTask(taskId, 'chat');
|
|
2075
|
+
});
|
|
2076
|
+
byId('aser-task-graph')?.addEventListener('click', (event) => {
|
|
2077
|
+
const target = event.target;
|
|
2078
|
+
if (!(target instanceof HTMLElement)) return;
|
|
2079
|
+
const node = target.closest('[data-aser-graph-task-id]');
|
|
2080
|
+
if (!(node instanceof HTMLElement)) return;
|
|
2081
|
+
const taskId = String(node.dataset.aserGraphTaskId || '').trim();
|
|
2082
|
+
if (!taskId) return;
|
|
2083
|
+
setFocusedTask(taskId, 'graph');
|
|
2084
|
+
});
|
|
2085
|
+
byId('aser-task-thread')?.addEventListener('click', (event) => {
|
|
2086
|
+
const target = event.target;
|
|
2087
|
+
if (!(target instanceof HTMLElement)) return;
|
|
2088
|
+
const row = target.closest('[data-aser-thread-task-id]');
|
|
2089
|
+
if (!(row instanceof HTMLElement)) return;
|
|
2090
|
+
const taskId = String(row.dataset.aserThreadTaskId || '').trim();
|
|
2091
|
+
if (!taskId) return;
|
|
2092
|
+
setFocusedTask(taskId, 'thread');
|
|
2093
|
+
});
|
|
2094
|
+
byId('aser-thread-summary')?.addEventListener('click', (event) => {
|
|
2095
|
+
const target = event.target;
|
|
2096
|
+
if (!(target instanceof HTMLElement)) return;
|
|
2097
|
+
const row = target.closest('[data-aser-thread-root-id]');
|
|
2098
|
+
if (!(row instanceof HTMLElement)) return;
|
|
2099
|
+
const taskId = String(row.dataset.aserThreadRootId || '').trim();
|
|
2100
|
+
if (!taskId) return;
|
|
2101
|
+
state.viewMode = 'thread';
|
|
2102
|
+
setFocusedTask(taskId, 'thread');
|
|
2103
|
+
});
|
|
2104
|
+
byId('aser-focus-detail')?.addEventListener('click', (event) => {
|
|
2105
|
+
const target = event.target;
|
|
2106
|
+
if (!(target instanceof HTMLElement)) return;
|
|
2107
|
+
const action = String(target.dataset.focusAction || '').trim();
|
|
2108
|
+
const taskId = String(target.dataset.focusTaskId || '').trim();
|
|
2109
|
+
if (!action || !taskId) return;
|
|
2110
|
+
if (action === 'add-artifact') {
|
|
2111
|
+
addTaskArtifact(taskId).catch((error) => alert(`添加产物失败: ${error?.message || error}`));
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
if (action === 'replay-task') {
|
|
2115
|
+
startDemo(taskId);
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
if (action === 'loop-clarify') {
|
|
2119
|
+
submitLoopActionFromTask(taskId, 'clarify').catch((error) => alert(`发起澄清回路失败: ${error?.message || error}`));
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
if (action === 'loop-escalate') {
|
|
2123
|
+
submitLoopActionFromTask(taskId, 'escalate').catch((error) => alert(`发起上升回路失败: ${error?.message || error}`));
|
|
2124
|
+
return;
|
|
2125
|
+
}
|
|
2126
|
+
if (action === 'eval-accept') {
|
|
2127
|
+
evaluateTask(taskId, 'accept').catch((error) => alert(`任务评价失败: ${error?.message || error}`));
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
if (action === 'eval-revise') {
|
|
2131
|
+
evaluateTask(taskId, 'revise').catch((error) => alert(`任务评价失败: ${error?.message || error}`));
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
function mount() {
|
|
2137
|
+
bindEvents();
|
|
2138
|
+
renderAll();
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
function refreshTeamContext() {
|
|
2142
|
+
refreshSelectedRunTeam();
|
|
2143
|
+
renderAll();
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
return {
|
|
2147
|
+
mount,
|
|
2148
|
+
setAccessContext,
|
|
2149
|
+
loadProjects,
|
|
2150
|
+
refreshTeamContext,
|
|
2151
|
+
};
|
|
2152
|
+
}
|