openteam 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * OpenTeam Dashboard
3
+ *
4
+ * 实时显示团队状态、Agent 状态和消息流
5
+ */
6
+
7
+ import { getServeUrl, isServeRunning } from '../team/serve.js';
8
+ import { createDashboard, updateHeader, updateTeamStatus, updateAgentStatus, updateMessageStream } from './ui.js';
9
+ import { fetchTeamStatus, fetchAgentStatus, fetchMessageStream } from './data.js';
10
+
11
+ const REFRESH_INTERVAL = 3000; // 3 秒
12
+
13
+ /**
14
+ * 启动 Dashboard
15
+ */
16
+ export async function dashboard(teamName) {
17
+ // 检查团队是否运行
18
+ if (!isServeRunning(teamName)) {
19
+ console.error(`\x1b[31m错误:\x1b[0m 团队 ${teamName} 未运行`);
20
+ console.log(`请先运行: openteam start ${teamName}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ const serveUrl = getServeUrl(teamName);
25
+
26
+ // 创建 UI
27
+ const ui = createDashboard(teamName);
28
+
29
+ // 初始渲染
30
+ await refreshDashboard(ui, teamName, serveUrl);
31
+
32
+ // 定期刷新
33
+ const intervalId = setInterval(async () => {
34
+ await refreshDashboard(ui, teamName, serveUrl);
35
+ }, REFRESH_INTERVAL);
36
+
37
+ // 清理资源
38
+ process.on('exit', () => {
39
+ clearInterval(intervalId);
40
+ ui.screen.destroy();
41
+ });
42
+
43
+ process.on('SIGINT', () => {
44
+ clearInterval(intervalId);
45
+ ui.screen.destroy();
46
+ process.exit(0);
47
+ });
48
+ }
49
+
50
+ /**
51
+ * 刷新 Dashboard 数据并更新 UI
52
+ */
53
+ async function refreshDashboard(ui, teamName, serveUrl) {
54
+ try {
55
+ const refreshTime = new Date().toLocaleString('zh-CN', { hour12: false });
56
+
57
+ // 并行获取数据
58
+ const [teamStatus, agentStatuses, messages] = await Promise.all([
59
+ fetchTeamStatus(teamName),
60
+ fetchAgentStatus(teamName, serveUrl),
61
+ fetchMessageStream(teamName, serveUrl, 20),
62
+ ]);
63
+
64
+ // 更新 UI
65
+ updateHeader(ui.header, teamName, refreshTime);
66
+ updateTeamStatus(ui.teamStatus, teamStatus);
67
+ updateAgentStatus(ui.agentStatus, agentStatuses);
68
+ updateMessageStream(ui.messageStream, messages);
69
+
70
+ ui.screen.render();
71
+ } catch (err) {
72
+ // 显示错误但不退出
73
+ updateHeader(ui.header, teamName, `Error: ${err.message}`);
74
+ ui.screen.render();
75
+ }
76
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Dashboard UI components (using blessed)
3
+ */
4
+
5
+ import blessed from 'blessed';
6
+
7
+ // 模块级变量:保存当前消息列表原始数据,供展开详情用
8
+ let _currentMessages = [];
9
+
10
+ /**
11
+ * 创建 Dashboard 界面
12
+ */
13
+ export function createDashboard(teamName) {
14
+ const screen = blessed.screen({
15
+ smartCSR: true,
16
+ fullUnicode: true,
17
+ title: `OpenTeam Dashboard - ${teamName}`,
18
+ });
19
+
20
+ // Header box
21
+ const header = blessed.box({
22
+ top: 0,
23
+ left: 0,
24
+ width: '100%',
25
+ height: 3,
26
+ content: '',
27
+ tags: true,
28
+ border: { type: 'line' },
29
+ style: {
30
+ fg: 'white',
31
+ border: { fg: 'cyan' },
32
+ },
33
+ });
34
+
35
+ // Team status box
36
+ const teamStatus = blessed.box({
37
+ top: 3,
38
+ left: 0,
39
+ width: '100%',
40
+ height: 8,
41
+ content: '',
42
+ tags: true,
43
+ border: { type: 'line' },
44
+ label: ' 团队状态 ',
45
+ style: {
46
+ fg: 'white',
47
+ border: { fg: 'green' },
48
+ },
49
+ });
50
+
51
+ // Agent status box
52
+ const agentStatus = blessed.box({
53
+ top: 11,
54
+ left: 0,
55
+ width: '100%',
56
+ height: 12,
57
+ content: '',
58
+ tags: true,
59
+ border: { type: 'line' },
60
+ label: ' Agent 状态 ',
61
+ scrollable: true,
62
+ keys: true,
63
+ vi: true,
64
+ alwaysScroll: true,
65
+ scrollbar: { ch: ' ', style: { bg: 'blue' } },
66
+ style: {
67
+ fg: 'white',
68
+ border: { fg: 'yellow' },
69
+ },
70
+ });
71
+
72
+ // 消息流列表(可选中)
73
+ const messageStream = blessed.list({
74
+ top: 23,
75
+ left: 0,
76
+ width: '100%',
77
+ height: '100%-23',
78
+ tags: true,
79
+ border: { type: 'line' },
80
+ label: ' 消息流 (↑↓选择 Enter展开 q退出) ',
81
+ scrollable: true,
82
+ keys: true,
83
+ vi: true,
84
+ alwaysScroll: true,
85
+ scrollbar: { ch: ' ', style: { bg: 'blue' } },
86
+ style: {
87
+ fg: 'white',
88
+ border: { fg: 'magenta' },
89
+ selected: { bg: 'blue', fg: 'white' },
90
+ item: { fg: 'white' },
91
+ },
92
+ items: [],
93
+ });
94
+
95
+ // 消息详情弹窗(默认隐藏)
96
+ const detailBox = blessed.box({
97
+ top: 'center',
98
+ left: 'center',
99
+ width: '80%',
100
+ height: '70%',
101
+ content: '',
102
+ tags: true,
103
+ border: { type: 'line' },
104
+ label: ' 消息详情 (Esc/q 关闭) ',
105
+ scrollable: true,
106
+ keys: true,
107
+ vi: true,
108
+ alwaysScroll: true,
109
+ scrollbar: { ch: ' ', style: { bg: 'blue' } },
110
+ style: {
111
+ fg: 'white',
112
+ bg: 'black',
113
+ border: { fg: 'cyan' },
114
+ },
115
+ hidden: true,
116
+ });
117
+
118
+ screen.append(header);
119
+ screen.append(teamStatus);
120
+ screen.append(agentStatus);
121
+ screen.append(messageStream);
122
+ screen.append(detailBox);
123
+
124
+ // 全局退出
125
+ screen.key(['q', 'C-c'], () => {
126
+ if (!detailBox.hidden) {
127
+ // 如果详情弹窗打开,先关闭弹窗
128
+ detailBox.hide();
129
+ messageStream.focus();
130
+ screen.render();
131
+ return;
132
+ }
133
+ return process.exit(0);
134
+ });
135
+
136
+ // Enter 展开消息详情
137
+ messageStream.on('select', (item, index) => {
138
+ const msg = _currentMessages[index];
139
+ if (!msg) return;
140
+
141
+ const time = new Date(msg.timestamp).toLocaleTimeString('zh-CN', { hour12: false });
142
+ const detail = [
143
+ `{bold}时间:{/bold} ${time}`,
144
+ `{bold}发送方:{/bold} {cyan-fg}${msg.from}{/cyan-fg}`,
145
+ `{bold}接收方:{/bold} {cyan-fg}${msg.to}{/cyan-fg}`,
146
+ '',
147
+ '{bold}内容:{/bold}',
148
+ '─'.repeat(60),
149
+ msg.fullContent || msg.content,
150
+ ].join('\n');
151
+
152
+ detailBox.setContent(detail);
153
+ detailBox.setScrollPerc(0);
154
+ detailBox.show();
155
+ detailBox.focus();
156
+ screen.render();
157
+ });
158
+
159
+ // Esc 关闭详情弹窗
160
+ detailBox.key(['escape', 'q'], () => {
161
+ detailBox.hide();
162
+ messageStream.focus();
163
+ screen.render();
164
+ });
165
+
166
+ // 默认焦点在消息流
167
+ messageStream.focus();
168
+ screen.render();
169
+
170
+ return {
171
+ screen,
172
+ header,
173
+ teamStatus,
174
+ agentStatus,
175
+ messageStream,
176
+ detailBox,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * 更新 Header 内容
182
+ */
183
+ export function updateHeader(headerBox, teamName, refreshTime) {
184
+ const content = `{center}{bold}OpenTeam Dashboard - ${teamName}{/bold}\nLast refresh: ${refreshTime} | Press 'q' to quit{/center}`;
185
+ headerBox.setContent(content);
186
+ }
187
+
188
+ /**
189
+ * 更新团队状态
190
+ */
191
+ export function updateTeamStatus(box, teamStatus) {
192
+ if (!teamStatus.running) {
193
+ box.setContent(`{red-fg}${teamStatus.error}{/red-fg}\n\n请运行: openteam start ${teamStatus.teamName || '<team>'}`);
194
+ return;
195
+ }
196
+
197
+ const content = [
198
+ `{green-fg}● 运行中{/green-fg}`,
199
+ `Serve URL: ${teamStatus.url}`,
200
+ `PID: ${teamStatus.pid}`,
201
+ `Leader: ${teamStatus.leader}`,
202
+ `项目目录: ${teamStatus.projectDir}`,
203
+ `启动时间: ${teamStatus.started}`,
204
+ ].join('\n');
205
+
206
+ box.setContent(content);
207
+ }
208
+
209
+ /**
210
+ * 更新 Agent 状态
211
+ */
212
+ export function updateAgentStatus(box, agentStatuses) {
213
+ if (agentStatuses.length === 0) {
214
+ box.setContent('{yellow-fg}暂无活跃 Agent{/yellow-fg}');
215
+ return;
216
+ }
217
+
218
+ const lines = agentStatuses.map((agent) => {
219
+ const status = agent.online ? '{green-fg}●{/green-fg}' : '{red-fg}○{/red-fg}';
220
+ const name = agent.name.padEnd(15);
221
+ const sessionId = agent.sessionId.slice(0, 12).padEnd(12);
222
+ const cwd = agent.cwd.length > 40 ? '...' + agent.cwd.slice(-37) : agent.cwd;
223
+
224
+ return `${status} ${name} ${sessionId} ${cwd}`;
225
+ });
226
+
227
+ const header = '{bold}状态 Agent 会话ID 工作目录{/bold}';
228
+ box.setContent([header, ...lines].join('\n'));
229
+ }
230
+
231
+ /**
232
+ * 更新消息流
233
+ */
234
+ export function updateMessageStream(listBox, messages) {
235
+ // 保存原始数据供展开用
236
+ _currentMessages = messages;
237
+
238
+ if (messages.length === 0) {
239
+ listBox.setItems(['{yellow-fg}暂无消息{/yellow-fg}']);
240
+ return;
241
+ }
242
+
243
+ // 记住当前选中位置
244
+ const prevSelected = listBox.selected;
245
+ const wasAtBottom = prevSelected >= listBox.items.length - 2;
246
+
247
+ const items = messages.map((msg) => {
248
+ const time = new Date(msg.timestamp).toLocaleTimeString('zh-CN', { hour12: false });
249
+ const fromTag = msg.from === 'boss'
250
+ ? '{black-fg}{yellow-bg} [from boss] {/}'
251
+ : `{black-fg}{green-bg} [from ${msg.from}] {/}`;
252
+ const toTag = `{cyan-fg}→ ${msg.to}{/cyan-fg}`;
253
+ const content = msg.content.replace(/\n/g, ' ').slice(0, 80);
254
+
255
+ return `{gray-fg}${time}{/gray-fg} ${fromTag} ${toTag} ${content}`;
256
+ });
257
+
258
+ listBox.setItems(items);
259
+
260
+ // 如果之前在底部,跟随滚到底
261
+ if (wasAtBottom || prevSelected === 0) {
262
+ listBox.select(items.length - 1);
263
+ listBox.setScrollPerc(100);
264
+ } else {
265
+ listBox.select(Math.min(prevSelected, items.length - 1));
266
+ }
267
+ }
package/src/index.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * OpenTeam Plugin for OpenCode
3
+ *
4
+ * 团队协作插件。记忆功能已迁移到 openmemory 插件。
5
+ */
6
+
7
+ import { tool } from '@opencode-ai/plugin';
8
+ import { createHooks } from './plugin/hooks.js';
9
+ import { createToolDefs } from './plugin/tools.js';
10
+
11
+ const OpenTeamPlugin = async (ctx) => {
12
+ // Only load when started via openteam (OPENTEAM_TEAM env var is set)
13
+ const teamName = process.env.OPENTEAM_TEAM;
14
+ if (!teamName) {
15
+ return {};
16
+ }
17
+
18
+ const hooks = createHooks(ctx);
19
+ const toolDefs = createToolDefs(ctx);
20
+
21
+ // Convert tool definitions to OpenCode format
22
+ const tools = {};
23
+ for (const [name, def] of Object.entries(toolDefs)) {
24
+ tools[name] = tool({
25
+ description: def.description,
26
+ args: def.args,
27
+ execute: def.execute,
28
+ });
29
+ }
30
+
31
+ return {
32
+ 'experimental.chat.system.transform': hooks.systemTransform,
33
+ 'experimental.chat.messages.transform': hooks.messagesTransform,
34
+ tool: tools,
35
+ };
36
+ };
37
+
38
+ export default OpenTeamPlugin;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Plugin hooks implementation — 团队协作功能
3
+ *
4
+ * 记忆功能已迁移到 openmemory 插件。
5
+ * openteam 只负责:
6
+ * - messagesTransform: 给无来源消息添加 [from boss]
7
+ * - systemTransform: 注入团队上下文 + 协作规则
8
+ */
9
+
10
+ import { loadTeamConfig } from '../team/config.js';
11
+ import { getCurrentAgent } from '../utils/agent.js';
12
+ import { createLogger } from '../utils/logger.js';
13
+
14
+ const log = createLogger('hooks');
15
+
16
+ /**
17
+ * Format team members prompt
18
+ */
19
+ function formatTeamPrompt(teamConfig, currentAgentName) {
20
+ if (!teamConfig?.agents?.length) return '';
21
+
22
+ const teamMembers = teamConfig.agents
23
+ .map((a) => (a === currentAgentName ? `- \`${a}\` (你)` : `- \`${a}\``))
24
+ .join('\n');
25
+
26
+ return `<team>\n团队成员:\n${teamMembers}\n</team>`;
27
+ }
28
+
29
+ /**
30
+ * Get team collaboration rules prompt
31
+ */
32
+ function getCollaborationRules() {
33
+ return `<collaboration-rules>
34
+ ## 团队协作规则
35
+
36
+ ### 消息来源识别
37
+ - \`[from xxx]\` 前缀表示消息来源
38
+ - \`[from boss]\` = 老板直接指示,优先级最高
39
+ - \`[from <agent>]\` = 来自其他 agent(如 architect、developer 等)
40
+
41
+ ### 通信方式
42
+ - **直接输出文字对方看不到**,必须用 \`msg\` 工具
43
+ - 收到 \`[from agent]\` 消息后,必须用 \`msg\` 回复对方才能看到
44
+
45
+ ### 任务汇报(重要)
46
+ - **任务完成后必须用 \`msg\` 向任务分配者汇报结果**
47
+ - 汇报内容:完成了什么、关键产出、是否有遗留问题
48
+ - 不汇报 = 对方不知道你完成了,协作链断裂
49
+
50
+ ### Boss 消息回复方式
51
+ - 收到 \`[from boss]\` 时**直接回复**即可(boss 在同一会话中)
52
+ - **禁止**用 \`msg(who="boss", ...)\`,boss 不是 agent
53
+
54
+ ### 记忆系统
55
+ - 系统会在对话结束后**自动巩固**有价值的信息到长期记忆,你无需刻意记录
56
+ - \`<memory>\` 中的 index 类型记忆显示了你所有笔记的摘要,需要详情时用 \`recall\` 查阅
57
+ - 用 \`review\` 和 \`reread\` 可以回顾历史对话
58
+ </collaboration-rules>`;
59
+ }
60
+
61
+ /**
62
+ * Create hooks for the plugin
63
+ */
64
+ export function createHooks() {
65
+ return {
66
+ /**
67
+ * Messages transform hook - add [from boss] prefix
68
+ */
69
+ messagesTransform: async (_input, output) => {
70
+ if (!output.messages || output.messages.length === 0) {
71
+ log.debug('messagesTransform: no messages');
72
+ return;
73
+ }
74
+
75
+ log.debug('messagesTransform called', { messageCount: output.messages.length });
76
+
77
+ for (let i = output.messages.length - 1; i >= 0; i--) {
78
+ const msg = output.messages[i];
79
+ if (msg.info?.role !== 'user') continue;
80
+
81
+ const textPart = msg.parts?.find((p) => p.type === 'text' && !p.synthetic);
82
+ if (!textPart?.text) {
83
+ log.debug('messagesTransform: user msg has no text part', { index: i, partTypes: msg.parts?.map(p => p.type) });
84
+ continue;
85
+ }
86
+
87
+ if (/^\[from\s+\w+\]/.test(textPart.text)) {
88
+ log.debug('messagesTransform: already tagged', { index: i, prefix: textPart.text.slice(0, 30) });
89
+ break;
90
+ }
91
+
92
+ textPart.text = `[from boss] ${textPart.text}`;
93
+ log.info('messagesTransform: tagged [from boss]', { index: i, preview: textPart.text.slice(0, 50) });
94
+ break;
95
+ }
96
+ },
97
+
98
+ /**
99
+ * System transform hook - inject team context + collaboration rules
100
+ */
101
+ systemTransform: async (input, output) => {
102
+ const { sessionID } = input;
103
+
104
+ log.debug('systemTransform called', { sessionID });
105
+ try {
106
+ const existingSystem = output.system?.join?.('') || output.system || '';
107
+
108
+ // 防止双份注入
109
+ if (existingSystem.includes('<collaboration-rules>')) {
110
+ log.debug('systemTransform: skip duplicate', { sessionID });
111
+ return;
112
+ }
113
+
114
+ // 跳过特殊请求
115
+ if (existingSystem.includes('title generator') || existingSystem.includes('You output ONLY')) {
116
+ log.debug('systemTransform: skip special request', { sessionID });
117
+ return;
118
+ }
119
+
120
+ const agent = await getCurrentAgent(sessionID);
121
+ if (!agent) {
122
+ log.debug('systemTransform: agent not found', { sessionID });
123
+ return;
124
+ }
125
+
126
+ // 注入团队成员
127
+ const teamConfig = loadTeamConfig(agent.team);
128
+ if (teamConfig) {
129
+ const teamPrompt = formatTeamPrompt(teamConfig, agent.name);
130
+ if (teamPrompt) {
131
+ (output.system ||= []).push(teamPrompt);
132
+ }
133
+
134
+ // 注入协作规则
135
+ (output.system ||= []).push(getCollaborationRules());
136
+ log.info('systemTransform: injected', { sessionID, agent: agent.full });
137
+ } else {
138
+ log.warn('systemTransform: team config not found', { sessionID, team: agent.team });
139
+ }
140
+ } catch (e) {
141
+ log.error('systemTransform error', { error: e.message });
142
+ }
143
+ },
144
+ };
145
+ }