@yeaft/webchat-agent 0.0.233 → 0.0.235
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/connection/buffer.js +87 -0
- package/connection/heartbeat.js +47 -0
- package/connection/index.js +89 -0
- package/connection/message-router.js +271 -0
- package/connection/upgrade-worker-template.js +103 -0
- package/connection/upgrade.js +294 -0
- package/connection.js +14 -777
- package/crew/control.js +364 -0
- package/crew/human-interaction.js +115 -0
- package/crew/persistence.js +287 -0
- package/crew/role-management.js +131 -0
- package/crew/role-output.js +315 -0
- package/crew/role-query.js +309 -0
- package/crew/routing.js +194 -0
- package/crew/session.js +474 -0
- package/crew/shared-dir.js +116 -0
- package/crew/task-files.js +370 -0
- package/crew/ui-messages.js +246 -0
- package/crew/worktree.js +130 -0
- package/package.json +6 -2
- package/service/config.js +133 -0
- package/service/index.js +99 -0
- package/service/linux.js +111 -0
- package/service/macos.js +137 -0
- package/service/windows.js +181 -0
- package/service.js +23 -624
- package/workbench/file-ops.js +436 -0
- package/workbench/file-search.js +66 -0
- package/workbench/git-ops.js +313 -0
- package/workbench/transfer.js +99 -0
- package/workbench/utils.js +41 -0
- package/workbench.js +15 -938
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew — 共享目录和 CLAUDE.md 管理
|
|
3
|
+
* initSharedDir, initRoleDir, writeSharedClaudeMd, writeRoleClaudeMd, updateSharedClaudeMd
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { getMessages } from '../crew-i18n.js';
|
|
8
|
+
|
|
9
|
+
/** Format role label: "icon displayName" or just "displayName" if no icon */
|
|
10
|
+
function roleLabel(r) {
|
|
11
|
+
return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 初始化共享目录
|
|
16
|
+
*/
|
|
17
|
+
export async function initSharedDir(sharedDir, roles, projectDir, language = 'zh-CN') {
|
|
18
|
+
await fs.mkdir(sharedDir, { recursive: true });
|
|
19
|
+
await fs.mkdir(join(sharedDir, 'context'), { recursive: true });
|
|
20
|
+
await fs.mkdir(join(sharedDir, 'sessions'), { recursive: true });
|
|
21
|
+
await fs.mkdir(join(sharedDir, 'roles'), { recursive: true });
|
|
22
|
+
|
|
23
|
+
// 初始化每个角色的目录
|
|
24
|
+
for (const role of roles) {
|
|
25
|
+
await initRoleDir(sharedDir, role, language);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 生成 .crew/CLAUDE.md(共享级)
|
|
29
|
+
await writeSharedClaudeMd(sharedDir, roles, projectDir, language);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 初始化角色目录: .crew/roles/{roleName}/CLAUDE.md
|
|
34
|
+
*/
|
|
35
|
+
export async function initRoleDir(sharedDir, role, language = 'zh-CN') {
|
|
36
|
+
const roleDir = join(sharedDir, 'roles', role.name);
|
|
37
|
+
await fs.mkdir(roleDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
// 角色 CLAUDE.md(仅首次创建,后续角色自己维护记忆内容)
|
|
40
|
+
const claudeMdPath = join(roleDir, 'CLAUDE.md');
|
|
41
|
+
try {
|
|
42
|
+
await fs.access(claudeMdPath);
|
|
43
|
+
// 已存在,不覆盖(保留角色自己写入的记忆)
|
|
44
|
+
} catch {
|
|
45
|
+
await writeRoleClaudeMd(sharedDir, role, language);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 写入 .crew/CLAUDE.md — 共享级(所有角色自动继承)
|
|
51
|
+
*/
|
|
52
|
+
export async function writeSharedClaudeMd(sharedDir, roles, projectDir, language = 'zh-CN') {
|
|
53
|
+
const m = getMessages(language);
|
|
54
|
+
|
|
55
|
+
const claudeMd = `${m.projectGoal}
|
|
56
|
+
|
|
57
|
+
${m.projectCodePath}
|
|
58
|
+
${projectDir}
|
|
59
|
+
${m.useAbsolutePath}
|
|
60
|
+
|
|
61
|
+
${m.teamMembersTitle}
|
|
62
|
+
${roles.length > 0 ? roles.map(r => `- ${roleLabel(r)}(${r.name}): ${r.description}${r.isDecisionMaker ? ` (${m.decisionMakerTag})` : ''}`).join('\n') : m.noMembers}
|
|
63
|
+
|
|
64
|
+
${m.workConventions}
|
|
65
|
+
${m.workConventionsContent}
|
|
66
|
+
|
|
67
|
+
${m.stuckRules}
|
|
68
|
+
${m.stuckRulesContent}
|
|
69
|
+
|
|
70
|
+
${m.worktreeRules}
|
|
71
|
+
${m.worktreeRulesContent}
|
|
72
|
+
|
|
73
|
+
${m.featureRecordShared}
|
|
74
|
+
|
|
75
|
+
${m.sharedMemoryTitle}
|
|
76
|
+
${m.sharedMemoryDefault}
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
await fs.writeFile(join(sharedDir, 'CLAUDE.md'), claudeMd);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 写入 .crew/roles/{roleName}/CLAUDE.md — 角色级
|
|
84
|
+
*/
|
|
85
|
+
export async function writeRoleClaudeMd(sharedDir, role, language = 'zh-CN') {
|
|
86
|
+
const roleDir = join(sharedDir, 'roles', role.name);
|
|
87
|
+
const m = getMessages(language);
|
|
88
|
+
|
|
89
|
+
let claudeMd = `${m.roleTitle(roleLabel(role))}
|
|
90
|
+
${role.claudeMd || role.description}
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
// 有独立 worktree 的角色,覆盖代码工作目录
|
|
94
|
+
if (role.workDir) {
|
|
95
|
+
claudeMd += `
|
|
96
|
+
${m.codeWorkDir}
|
|
97
|
+
${role.workDir}
|
|
98
|
+
${m.codeWorkDirNote}
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
claudeMd += `
|
|
103
|
+
${m.personalMemory}
|
|
104
|
+
${m.personalMemoryDefault}
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
await fs.writeFile(join(roleDir, 'CLAUDE.md'), claudeMd);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 角色变动时更新 .crew/CLAUDE.md
|
|
112
|
+
*/
|
|
113
|
+
export async function updateSharedClaudeMd(session) {
|
|
114
|
+
const roles = Array.from(session.roles.values());
|
|
115
|
+
await writeSharedClaudeMd(session.sharedDir, roles, session.projectDir, session.language || 'zh-CN');
|
|
116
|
+
}
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crew — Task 文件管理(系统自动管理)
|
|
3
|
+
* ensureTaskFile, appendTaskRecord, readTaskFile, parseCompletedTasks,
|
|
4
|
+
* updateFeatureIndex, appendChangelog, saveRoleWorkSummary,
|
|
5
|
+
* updateKanban, readKanban
|
|
6
|
+
*/
|
|
7
|
+
import { promises as fs } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { getMessages } from '../crew-i18n.js';
|
|
10
|
+
|
|
11
|
+
/** Format role label */
|
|
12
|
+
function roleLabel(r) {
|
|
13
|
+
return r.icon ? `${r.icon} ${r.displayName}` : r.displayName;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 自动创建 task 进度文件
|
|
18
|
+
*/
|
|
19
|
+
export async function ensureTaskFile(session, taskId, taskTitle, assignee, summary) {
|
|
20
|
+
const featuresDir = join(session.sharedDir, 'context', 'features');
|
|
21
|
+
const filePath = join(featuresDir, `${taskId}.md`);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(filePath);
|
|
25
|
+
// 文件已存在,不覆盖
|
|
26
|
+
return;
|
|
27
|
+
} catch {
|
|
28
|
+
// 文件不存在,创建
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await fs.mkdir(featuresDir, { recursive: true });
|
|
32
|
+
|
|
33
|
+
const m = getMessages(session.language || 'zh-CN');
|
|
34
|
+
const now = new Date().toISOString();
|
|
35
|
+
const content = `# ${m.featureLabel}: ${taskTitle}
|
|
36
|
+
- task-id: ${taskId}
|
|
37
|
+
- ${m.statusPending}
|
|
38
|
+
- ${m.assigneeLabel}: ${assignee}
|
|
39
|
+
- ${m.createdAtLabel}: ${now}
|
|
40
|
+
|
|
41
|
+
${m.requirementDesc}
|
|
42
|
+
${summary}
|
|
43
|
+
|
|
44
|
+
${m.workRecord}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
await fs.writeFile(filePath, content);
|
|
48
|
+
|
|
49
|
+
// 同步到 session.features
|
|
50
|
+
if (!session.features.has(taskId)) {
|
|
51
|
+
session.features.set(taskId, { taskId, taskTitle, createdAt: Date.now() });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(`[Crew] Task file created: ${taskId} (${taskTitle})`);
|
|
55
|
+
|
|
56
|
+
// 更新 feature 索引
|
|
57
|
+
updateFeatureIndex(session).catch(e => console.warn('[Crew] Failed to update feature index:', e.message));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 追加工作记录到 task 文件
|
|
62
|
+
*/
|
|
63
|
+
export async function appendTaskRecord(session, taskId, roleName, summary) {
|
|
64
|
+
const filePath = join(session.sharedDir, 'context', 'features', `${taskId}.md`);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await fs.access(filePath);
|
|
68
|
+
} catch {
|
|
69
|
+
// 文件不存在,跳过
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const role = session.roles.get(roleName);
|
|
74
|
+
const label = role ? roleLabel(role) : roleName;
|
|
75
|
+
const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
|
76
|
+
const record = `\n### ${label} - ${now}\n${summary}\n`;
|
|
77
|
+
|
|
78
|
+
await fs.appendFile(filePath, record);
|
|
79
|
+
console.log(`[Crew] Task record appended: ${taskId} by ${roleName}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 读取 task 文件内容(用于注入上下文)
|
|
84
|
+
*/
|
|
85
|
+
export async function readTaskFile(session, taskId) {
|
|
86
|
+
const filePath = join(session.sharedDir, 'context', 'features', `${taskId}.md`);
|
|
87
|
+
try {
|
|
88
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 从 TASKS block 文本中提取已完成任务的 taskId 集合
|
|
96
|
+
*/
|
|
97
|
+
export function parseCompletedTasks(text) {
|
|
98
|
+
const ids = new Set();
|
|
99
|
+
const match = text.match(/---TASKS---([\s\S]*?)---END_TASKS---/);
|
|
100
|
+
if (!match) return ids;
|
|
101
|
+
for (const line of match[1].split('\n')) {
|
|
102
|
+
const m = line.match(/^-\s*\[[xX]\]\s*.+#(\S+)/);
|
|
103
|
+
if (m) ids.add(m[1]);
|
|
104
|
+
}
|
|
105
|
+
return ids;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 更新 feature 索引文件 context/features/index.md
|
|
110
|
+
*/
|
|
111
|
+
export async function updateFeatureIndex(session) {
|
|
112
|
+
const featuresDir = join(session.sharedDir, 'context', 'features');
|
|
113
|
+
await fs.mkdir(featuresDir, { recursive: true });
|
|
114
|
+
|
|
115
|
+
const m = getMessages(session.language || 'zh-CN');
|
|
116
|
+
const completed = session._completedTaskIds || new Set();
|
|
117
|
+
const allFeatures = Array.from(session.features.values());
|
|
118
|
+
|
|
119
|
+
allFeatures.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0));
|
|
120
|
+
|
|
121
|
+
const inProgress = allFeatures.filter(f => !completed.has(f.taskId));
|
|
122
|
+
const done = allFeatures.filter(f => completed.has(f.taskId));
|
|
123
|
+
|
|
124
|
+
const locale = (session.language === 'en') ? 'en-US' : 'zh-CN';
|
|
125
|
+
const now = new Date().toLocaleString(locale, { timeZone: 'Asia/Shanghai' });
|
|
126
|
+
let content = `${m.featureIndex}\n> ${m.lastUpdated}: ${now}\n`;
|
|
127
|
+
|
|
128
|
+
content += `\n${m.inProgressGroup(inProgress.length)}\n`;
|
|
129
|
+
if (inProgress.length > 0) {
|
|
130
|
+
content += `| ${m.colTaskId} | ${m.colTitle} | ${m.colCreatedAt} |\n|---------|------|----------|\n`;
|
|
131
|
+
for (const f of inProgress) {
|
|
132
|
+
const date = f.createdAt ? new Date(f.createdAt).toLocaleDateString(locale) : '-';
|
|
133
|
+
content += `| ${f.taskId} | ${f.taskTitle} | ${date} |\n`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
content += `\n${m.completedGroup(done.length)}\n`;
|
|
138
|
+
if (done.length > 0) {
|
|
139
|
+
content += `| ${m.colTaskId} | ${m.colTitle} | ${m.colCreatedAt} |\n|---------|------|----------|\n`;
|
|
140
|
+
for (const f of done) {
|
|
141
|
+
const date = f.createdAt ? new Date(f.createdAt).toLocaleDateString(locale) : '-';
|
|
142
|
+
content += `| ${f.taskId} | ${f.taskTitle} | ${date} |\n`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await fs.writeFile(join(featuresDir, 'index.md'), content);
|
|
147
|
+
console.log(`[Crew] Feature index updated: ${inProgress.length} in progress, ${done.length} completed`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 追加完成汇总到 context/changelog.md
|
|
152
|
+
*/
|
|
153
|
+
export async function appendChangelog(session, taskId, taskTitle) {
|
|
154
|
+
const contextDir = join(session.sharedDir, 'context');
|
|
155
|
+
await fs.mkdir(contextDir, { recursive: true });
|
|
156
|
+
const changelogPath = join(contextDir, 'changelog.md');
|
|
157
|
+
|
|
158
|
+
const m = getMessages(session.language || 'zh-CN');
|
|
159
|
+
|
|
160
|
+
// 读取 feature 文件提取最后一条工作记录作为摘要
|
|
161
|
+
const taskContent = await readTaskFile(session, taskId);
|
|
162
|
+
let summaryText = '';
|
|
163
|
+
if (taskContent) {
|
|
164
|
+
const records = taskContent.split(/\n### /);
|
|
165
|
+
if (records.length > 1) {
|
|
166
|
+
const lastRecord = records[records.length - 1];
|
|
167
|
+
const lines = lastRecord.split('\n');
|
|
168
|
+
summaryText = lines.slice(1).join('\n').trim();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (!summaryText) {
|
|
172
|
+
summaryText = m.noSummary;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 限制摘要长度
|
|
176
|
+
if (summaryText.length > 500) {
|
|
177
|
+
summaryText = summaryText.substring(0, 497) + '...';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const locale = (session.language === 'en') ? 'en-US' : 'zh-CN';
|
|
181
|
+
const now = new Date().toLocaleString(locale, { timeZone: 'Asia/Shanghai' });
|
|
182
|
+
const entry = `\n## ${taskId}: ${taskTitle}\n- ${m.completedAt}: ${now}\n- ${m.summaryLabel}: ${summaryText}\n`;
|
|
183
|
+
|
|
184
|
+
let exists = false;
|
|
185
|
+
try {
|
|
186
|
+
await fs.access(changelogPath);
|
|
187
|
+
exists = true;
|
|
188
|
+
} catch {}
|
|
189
|
+
|
|
190
|
+
if (!exists) {
|
|
191
|
+
await fs.writeFile(changelogPath, `${m.changelogTitle}\n${entry}`);
|
|
192
|
+
} else {
|
|
193
|
+
await fs.appendFile(changelogPath, entry);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(`[Crew] Changelog appended: ${taskId} (${taskTitle})`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Context 超限 clear 前,将角色当前输出摘要保存到 feature 文件
|
|
201
|
+
*/
|
|
202
|
+
export async function saveRoleWorkSummary(session, roleName, accumulatedText) {
|
|
203
|
+
const roleState = session.roleStates.get(roleName);
|
|
204
|
+
const taskId = roleState?.currentTask?.taskId;
|
|
205
|
+
if (!taskId || !accumulatedText) return;
|
|
206
|
+
|
|
207
|
+
// 截取最后 2000 字符作为工作摘要
|
|
208
|
+
const summary = accumulatedText.length > 2000
|
|
209
|
+
? '...' + accumulatedText.slice(-2000)
|
|
210
|
+
: accumulatedText;
|
|
211
|
+
|
|
212
|
+
const m = getMessages(session.language || 'zh-CN');
|
|
213
|
+
await appendTaskRecord(session, taskId, roleName,
|
|
214
|
+
`[${m.kanbanAutoSave}] ${summary}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 看板写入锁:防止并发写入
|
|
218
|
+
let _kanbanWriteLock = Promise.resolve();
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 更新工作看板 context/kanban.md
|
|
222
|
+
*
|
|
223
|
+
* @param {object} session
|
|
224
|
+
* @param {object} [opts]
|
|
225
|
+
* @param {string} [opts.taskId] - 要更新的任务 ID
|
|
226
|
+
* @param {string} [opts.assignee] - 负责人
|
|
227
|
+
* @param {string} [opts.status] - 当前状态
|
|
228
|
+
* @param {string} [opts.summary] - 最新进展摘要
|
|
229
|
+
* @param {boolean} [opts.completed] - 是否标记为已完成
|
|
230
|
+
*/
|
|
231
|
+
export async function updateKanban(session, opts = {}) {
|
|
232
|
+
const doUpdate = async () => {
|
|
233
|
+
const contextDir = join(session.sharedDir, 'context');
|
|
234
|
+
await fs.mkdir(contextDir, { recursive: true });
|
|
235
|
+
const kanbanPath = join(contextDir, 'kanban.md');
|
|
236
|
+
const m = getMessages(session.language || 'zh-CN');
|
|
237
|
+
|
|
238
|
+
// 加载现有看板数据
|
|
239
|
+
let entries = new Map(); // taskId → { taskId, taskTitle, assignee, status, summary }
|
|
240
|
+
let completedEntries = new Map();
|
|
241
|
+
try {
|
|
242
|
+
const existing = await fs.readFile(kanbanPath, 'utf-8');
|
|
243
|
+
// 解析表格行
|
|
244
|
+
const lines = existing.split('\n');
|
|
245
|
+
let section = null;
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
if (line.startsWith('## ') && line.includes('🔨')) section = 'active';
|
|
248
|
+
else if (line.startsWith('## ') && line.includes('✅')) section = 'completed';
|
|
249
|
+
else if (line.startsWith('|') && !line.startsWith('|--') && section) {
|
|
250
|
+
const cols = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
251
|
+
if (cols.length >= 3 && cols[0] !== m.colTaskId && cols[0] !== 'task-id') {
|
|
252
|
+
const entry = {
|
|
253
|
+
taskId: cols[0],
|
|
254
|
+
taskTitle: cols[1],
|
|
255
|
+
assignee: cols[2] || '-',
|
|
256
|
+
status: cols[3] || '-',
|
|
257
|
+
summary: cols[4] || '-'
|
|
258
|
+
};
|
|
259
|
+
if (section === 'completed') {
|
|
260
|
+
completedEntries.set(entry.taskId, entry);
|
|
261
|
+
} else {
|
|
262
|
+
entries.set(entry.taskId, entry);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch { /* 文件不存在 */ }
|
|
268
|
+
|
|
269
|
+
// 从 session.features 补充缺失的任务
|
|
270
|
+
const completed = session._completedTaskIds || new Set();
|
|
271
|
+
for (const [taskId, feature] of session.features) {
|
|
272
|
+
if (completed.has(taskId)) {
|
|
273
|
+
if (!completedEntries.has(taskId)) {
|
|
274
|
+
completedEntries.set(taskId, {
|
|
275
|
+
taskId,
|
|
276
|
+
taskTitle: feature.taskTitle,
|
|
277
|
+
assignee: '-',
|
|
278
|
+
status: '✅',
|
|
279
|
+
summary: '-'
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
entries.delete(taskId);
|
|
283
|
+
} else if (!entries.has(taskId)) {
|
|
284
|
+
entries.set(taskId, {
|
|
285
|
+
taskId,
|
|
286
|
+
taskTitle: feature.taskTitle,
|
|
287
|
+
assignee: '-',
|
|
288
|
+
status: '-',
|
|
289
|
+
summary: '-'
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 应用更新
|
|
295
|
+
if (opts.taskId) {
|
|
296
|
+
if (opts.completed) {
|
|
297
|
+
const entry = entries.get(opts.taskId) || completedEntries.get(opts.taskId);
|
|
298
|
+
if (entry) {
|
|
299
|
+
entry.status = '✅';
|
|
300
|
+
if (opts.summary) entry.summary = opts.summary;
|
|
301
|
+
completedEntries.set(opts.taskId, entry);
|
|
302
|
+
entries.delete(opts.taskId);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
let entry = entries.get(opts.taskId);
|
|
306
|
+
if (!entry) {
|
|
307
|
+
const feature = session.features.get(opts.taskId);
|
|
308
|
+
entry = {
|
|
309
|
+
taskId: opts.taskId,
|
|
310
|
+
taskTitle: opts.taskTitle || feature?.taskTitle || opts.taskId,
|
|
311
|
+
assignee: '-',
|
|
312
|
+
status: '-',
|
|
313
|
+
summary: '-'
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (opts.assignee) entry.assignee = opts.assignee;
|
|
317
|
+
if (opts.status) entry.status = opts.status;
|
|
318
|
+
if (opts.summary) {
|
|
319
|
+
// 截取摘要
|
|
320
|
+
entry.summary = opts.summary.length > 100
|
|
321
|
+
? opts.summary.substring(0, 97) + '...'
|
|
322
|
+
: opts.summary;
|
|
323
|
+
}
|
|
324
|
+
entries.set(opts.taskId, entry);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 生成看板文件
|
|
329
|
+
const locale = (session.language === 'en') ? 'en-US' : 'zh-CN';
|
|
330
|
+
const now = new Date().toLocaleString(locale, { timeZone: 'Asia/Shanghai' });
|
|
331
|
+
let content = `${m.kanbanTitle}\n> ${m.lastUpdated}: ${now}\n`;
|
|
332
|
+
|
|
333
|
+
const activeArr = Array.from(entries.values());
|
|
334
|
+
content += `\n## 🔨 ${m.kanbanActive} (${activeArr.length})\n`;
|
|
335
|
+
if (activeArr.length > 0) {
|
|
336
|
+
content += `| ${m.colTaskId} | ${m.colTitle} | ${m.kanbanColAssignee} | ${m.kanbanColStatus} | ${m.kanbanColSummary} |\n|---------|------|--------|------|----------|\n`;
|
|
337
|
+
for (const e of activeArr) {
|
|
338
|
+
content += `| ${e.taskId} | ${e.taskTitle} | ${e.assignee} | ${e.status} | ${e.summary} |\n`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const doneArr = Array.from(completedEntries.values());
|
|
343
|
+
content += `\n## ✅ ${m.kanbanCompleted} (${doneArr.length})\n`;
|
|
344
|
+
if (doneArr.length > 0) {
|
|
345
|
+
content += `| ${m.colTaskId} | ${m.colTitle} | ${m.kanbanColAssignee} |\n|---------|------|--------|\n`;
|
|
346
|
+
for (const e of doneArr) {
|
|
347
|
+
content += `| ${e.taskId} | ${e.taskTitle} | ${e.assignee} |\n`;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
await fs.writeFile(kanbanPath, content);
|
|
352
|
+
console.log(`[Crew] Kanban updated: ${activeArr.length} active, ${doneArr.length} completed`);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// 串行化写入
|
|
356
|
+
_kanbanWriteLock = _kanbanWriteLock.then(doUpdate, doUpdate);
|
|
357
|
+
return _kanbanWriteLock;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 读取看板文件内容
|
|
362
|
+
*/
|
|
363
|
+
export async function readKanban(session) {
|
|
364
|
+
const kanbanPath = join(session.sharedDir, 'context', 'kanban.md');
|
|
365
|
+
try {
|
|
366
|
+
return await fs.readFile(kanbanPath, 'utf-8');
|
|
367
|
+
} catch {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|