@bolloon/bolloon-agent 0.1.1 → 0.1.3
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/bin/bolloon-cli.cjs +165 -0
- package/bin/bolloon-daemon.sh +207 -0
- package/bin/bolloon.cmd +11 -0
- package/dist/agents/constraint-layer.js +10 -15
- package/dist/agents/pi-sdk.js +433 -106
- package/dist/agents/protocol.js +82 -1
- package/dist/agents/subagent-manager.js +2 -2
- package/dist/agents/workflow-engine.js +15 -20
- package/dist/agents/workflow-pivot-loop.js +541 -0
- package/dist/bollharness/src/index.js +5 -0
- package/dist/bollharness/src/scripts/checks/check_adr_plan_numbering.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_api_types.js +45 -0
- package/dist/bollharness/src/scripts/checks/check_artifact_link.js +146 -0
- package/dist/bollharness/src/scripts/checks/check_bridge_deps.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_bugfix_binding.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_bugfix_binding_ci.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_doc_file_references.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +135 -0
- package/dist/bollharness/src/scripts/checks/check_doc_links.js +31 -0
- package/dist/bollharness/src/scripts/checks/check_file_existence_claims.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_fragment_integrity.js +34 -0
- package/dist/bollharness/src/scripts/checks/check_hook_installed.js +63 -0
- package/dist/bollharness/src/scripts/checks/check_issue_closure.js +41 -0
- package/dist/bollharness/src/scripts/checks/check_mcp_parity.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_security.js +48 -0
- package/dist/bollharness/src/scripts/checks/check_skill_parity.js +6 -0
- package/dist/bollharness/src/scripts/checks/check_versions.js +6 -0
- package/dist/bollharness/src/scripts/checks/finding.js +13 -0
- package/dist/bollharness/src/scripts/checks/next_decision_number.js +20 -0
- package/dist/bollharness/src/scripts/checks/regenerate_magic_docs.js +6 -0
- package/dist/bollharness/src/scripts/ci/detect_rebaseline_triggers.js +8 -0
- package/dist/bollharness/src/scripts/ci/scan_subprocess_cfg.js +8 -0
- package/dist/bollharness/src/scripts/ci/scan_verify_artifacts.js +8 -0
- package/dist/bollharness/src/scripts/ci/scan_yaml_schema.js +8 -0
- package/dist/bollharness/src/scripts/context_router.js +67 -0
- package/dist/bollharness/src/scripts/deploy-guard.js +157 -0
- package/dist/bollharness/src/scripts/guard-feedback.js +192 -0
- package/dist/bollharness/src/scripts/guard_router.js +158 -0
- package/dist/bollharness/src/scripts/hooks/_hook_output.js +6 -0
- package/dist/bollharness/src/scripts/hooks/auto-python3.js +6 -0
- package/dist/bollharness/src/scripts/hooks/deploy-progress-on-session-end.js +6 -0
- package/dist/bollharness/src/scripts/hooks/failure-analyzer.js +6 -0
- package/dist/bollharness/src/scripts/hooks/gate-judgment-inject.js +92 -0
- package/dist/bollharness/src/scripts/hooks/gate-transition-judgment.js +63 -0
- package/dist/bollharness/src/scripts/hooks/inbox-ack.js +6 -0
- package/dist/bollharness/src/scripts/hooks/inbox-inject-on-start.js +6 -0
- package/dist/bollharness/src/scripts/hooks/inbox-validate.js +6 -0
- package/dist/bollharness/src/scripts/hooks/inbox-write-ledger.js +6 -0
- package/dist/bollharness/src/scripts/hooks/initializer-agent.js +6 -0
- package/dist/bollharness/src/scripts/hooks/loop-detection.js +73 -0
- package/dist/bollharness/src/scripts/hooks/owner-guard.js +6 -0
- package/dist/bollharness/src/scripts/hooks/precompact.js +6 -0
- package/dist/bollharness/src/scripts/hooks/review-agent-gatekeeper.js +6 -0
- package/dist/bollharness/src/scripts/hooks/risk-tracker.js +108 -0
- package/dist/bollharness/src/scripts/hooks/sanitize-on-read.js +6 -0
- package/dist/bollharness/src/scripts/hooks/session-reflection.js +7 -0
- package/dist/bollharness/src/scripts/hooks/session-start-magic-docs.js +7 -0
- package/dist/bollharness/src/scripts/hooks/session-start-reset-risk.js +7 -0
- package/dist/bollharness/src/scripts/hooks/session-start-toolkit-reminder.js +7 -0
- package/dist/bollharness/src/scripts/hooks/stop-evaluator.js +157 -0
- package/dist/bollharness/src/scripts/hooks/tool-call-counter.js +6 -0
- package/dist/bollharness/src/scripts/hooks/trace-analyzer.js +10 -0
- package/dist/bollharness/src/scripts/install/install-trust-token.js +7 -0
- package/dist/bollharness/src/scripts/install/multi_project_registry.js +9 -0
- package/dist/bollharness/src/scripts/install/phase2_auto.js +21 -0
- package/dist/bollharness/src/scripts/install/pre_commit_installer.js +6 -0
- package/dist/bollharness/src/scripts/install/tier_selector.js +7 -0
- package/dist/bollharness/src/scripts/install/transcript_miner.js +7 -0
- package/dist/bollharness/src/scripts/lib/claim_patterns.js +10 -0
- package/dist/bollharness/src/scripts/lib/sanitize_patterns.js +12 -0
- package/dist/bollharness/src/scripts/sanitize.js +6 -0
- package/dist/bollharness-integration/channel-judgment-engine.js +530 -0
- package/dist/bollharness-integration/context-chain-router.js +383 -0
- package/dist/bollharness-integration/context-router-judgment.js +13 -21
- package/dist/bollharness-integration/context-router.js +22 -64
- package/dist/bollharness-integration/gate-state-machine.js +14 -19
- package/dist/bollharness-integration/gate-transition-hooks.js +16 -61
- package/dist/bollharness-integration/guard-checker.js +21 -68
- package/dist/bollharness-integration/index.js +14 -124
- package/dist/bollharness-integration/integration.js +13 -20
- package/dist/bollharness-integration/llm-judgment-engine.js +569 -0
- package/dist/bollharness-integration/skill-adapter.js +18 -64
- package/dist/cli-entry.js +261 -0
- package/dist/constraint-runtime/src/commands.js +17 -7
- package/dist/constraint-runtime/src/constraint/budget.js +1 -6
- package/dist/constraint-runtime/src/constraint/permission.js +1 -6
- package/dist/constraint-runtime/src/models.js +1 -3
- package/dist/constraint-runtime/src/tools.js +17 -7
- package/dist/constraints/index.js +1 -7
- package/dist/documents/reader.js +8 -49
- package/dist/heartbeat/DaemonManager.js +242 -0
- package/dist/heartbeat/HealthMonitor.js +285 -0
- package/dist/heartbeat/StartupVerifier.js +205 -0
- package/dist/heartbeat/Watchdog.js +168 -0
- package/dist/heartbeat/index.js +84 -0
- package/dist/heartbeat/types.js +5 -0
- package/dist/index.js +381 -28
- package/dist/llm/config-store.js +31 -57
- package/dist/llm/llm-judgment-client.js +389 -0
- package/dist/llm/pi-ai.js +9 -52
- package/dist/network/agent-network.js +46 -90
- package/dist/network/hybrid-messenger.js +125 -0
- package/dist/network/iroh-bootstrap.js +38 -0
- package/dist/network/iroh-discovery.js +145 -0
- package/dist/network/iroh-integration.js +9 -16
- package/dist/network/iroh-transport.js +10 -48
- package/dist/network/p2p.js +23 -62
- package/dist/network/storage/adapters/json-adapter.js +4 -42
- package/dist/network/storage/index.js +147 -0
- package/dist/network/storage/types.js +14 -0
- package/dist/pi-ecosystem/index.js +233 -0
- package/dist/pi-ecosystem-colony/index.js +29 -90
- package/dist/pi-ecosystem-goals/index.js +20 -74
- package/dist/pi-ecosystem-judgment/decision.js +29 -47
- package/dist/pi-ecosystem-judgment/distillation.js +16 -29
- package/dist/pi-ecosystem-judgment/human-value-store.js +13 -60
- package/dist/pi-ecosystem-judgment/index.js +21 -74
- package/dist/pi-ecosystem-judgment/value-injection.js +26 -72
- package/dist/pi-ecosystem-mcp/index.js +24 -78
- package/dist/pi-ecosystem-subagents/index.js +20 -69
- package/dist/social/ant-colony/AdaptiveHeartbeat.js +3 -8
- package/dist/social/ant-colony/PheromoneEngine.js +11 -49
- package/dist/social/ant-colony/index.js +6 -0
- package/dist/social/ant-colony/types.js +4 -8
- package/dist/social/channels/ChannelManager.js +8 -46
- package/dist/social/channels/DiapChannelBridge.js +9 -47
- package/dist/social/channels/InterestMatcher.js +2 -7
- package/dist/social/channels/channel-agent-session.js +309 -0
- package/dist/social/channels/channel-heartbeat-agent.js +494 -0
- package/dist/social/channels/diap-doc-parser.js +204 -0
- package/dist/social/channels/harness-workflow-integrator.js +446 -0
- package/dist/social/channels/index.js +9 -0
- package/dist/social/channels/types.js +3 -7
- package/dist/social/global-shared-context.js +6 -47
- package/dist/social/heartbeat.js +29 -72
- package/dist/social/persona/enhanced-persona.js +299 -0
- package/dist/web/client.js +302 -136
- package/dist/web/components/p2p/index.js +159 -9
- package/dist/web/components/p2p/p2p-connection.js +136 -0
- package/dist/web/components/p2p/p2p-manager.js +24 -0
- package/dist/web/components/p2p/p2p-store-memory.js +1 -1
- package/dist/web/components/p2p/types.js +7 -0
- package/dist/web/index.html +5 -0
- package/dist/web/style.css +118 -0
- package/package.json +12 -6
- package/scripts/build-cli.js +206 -0
- package/scripts/postinstall.js +153 -0
- package/src/agents/pi-sdk.ts +347 -28
- package/src/agents/protocol.ts +95 -1
- package/src/agents/workflow-pivot-loop.ts +674 -0
- package/src/bollharness/CLAUDE.md +73 -0
- package/src/bollharness/README.md +143 -0
- package/src/bollharness/README.zh-CN.md +131 -0
- package/src/bollharness/reference/boll-reference/scripts/hooks/stop-evaluator.md +57 -0
- package/src/bollharness/scripts/context-fragments/artifact-linkage.md +14 -0
- package/src/bollharness/scripts/context-fragments/auth-consumers.md +17 -0
- package/src/bollharness/scripts/context-fragments/bridge-constitution.md +13 -0
- package/src/bollharness/scripts/context-fragments/catalyst-distributed.md +18 -0
- package/src/bollharness/scripts/context-fragments/closure-checklist.md +13 -0
- package/src/bollharness/scripts/context-fragments/contract-consumers.md +15 -0
- package/src/bollharness/scripts/context-fragments/db-shared-structures.md +15 -0
- package/src/bollharness/scripts/context-fragments/fixed-three-layers.md +19 -0
- package/src/bollharness/scripts/context-fragments/general-dev-principles.md +11 -0
- package/src/bollharness/scripts/context-fragments/issue-first.md +8 -0
- package/src/bollharness/scripts/context-fragments/mcp-parity.md +16 -0
- package/src/bollharness/scripts/context-fragments/pi-agent-operations.md +108 -0
- package/src/bollharness/scripts/context-fragments/protocol-consumers.md +15 -0
- package/src/bollharness/scripts/context-fragments/run-events-consumers.md +15 -0
- package/src/bollharness/scripts/context-fragments/scene-fidelity.md +13 -0
- package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +15 -0
- package/src/bollharness/scripts/context-fragments/two-language.md +15 -0
- package/src/bollharness/scripts/context-fragments/version-sources.md +14 -0
- package/src/bollharness/scripts/hooks/stop-evaluator.md +83 -0
- package/src/bollharness/templates/scaffold/CLAUDE.md +89 -0
- package/src/cli-entry.ts +304 -0
- package/src/heartbeat/DaemonManager.ts +283 -0
- package/src/heartbeat/HealthMonitor.ts +316 -0
- package/src/heartbeat/StartupVerifier.ts +223 -0
- package/src/heartbeat/Watchdog.ts +198 -0
- package/src/heartbeat/index.ts +108 -0
- package/src/heartbeat/types.ts +82 -0
- package/src/llm/config-store.ts +23 -5
- package/src/network/iroh-transport.ts +3 -3
- package/src/web/client.js +302 -136
- package/src/web/components/p2p/P2PModal.tsx +91 -3
- package/src/web/components/p2p/index.ts +171 -9
- package/src/web/components/p2p/p2p-connection.ts +153 -1
- package/src/web/components/p2p/p2p-manager.ts +39 -1
- package/src/web/components/p2p/p2p-store-memory.ts +1 -1
- package/src/web/components/p2p/p2p-tools.ts +315 -0
- package/src/web/components/p2p/types.ts +58 -0
- package/src/web/design.md +99 -0
- package/src/web/index.html +5 -0
- package/src/web/server.ts +353 -36
- package/src/web/style.css +118 -0
- package/tsconfig.cli.json +16 -0
- package/tsconfig.electron.json +1 -1
- package/tsconfig.json +1 -2
- package/dist/web/server.js +0 -1647
- package/dist/web/server.js.map +0 -1
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel Heartbeat Agent - 集成 Harness 判断力的自动心跳智能体
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* 1. 自动心跳广播 persona 到 IPFS
|
|
6
|
+
* 2. 自动发现外部智能体并解析 DiapDoc
|
|
7
|
+
* 3. 基于判断力引擎自动调用 Harness
|
|
8
|
+
* 4. 与外部智能体自动多轮对话
|
|
9
|
+
*/
|
|
10
|
+
import { createChannelAgent } from './channel-agent-session.js';
|
|
11
|
+
import { createDiapDocParser } from './diap-doc-parser.js';
|
|
12
|
+
import { createChannelJudgmentEngine } from '../../bollharness-integration/channel-judgment-engine.js';
|
|
13
|
+
/**
|
|
14
|
+
* Channel Heartbeat Agent - 自动心跳 + 判断力驱动的多轮对话
|
|
15
|
+
*/
|
|
16
|
+
export class ChannelHeartbeatAgent {
|
|
17
|
+
config;
|
|
18
|
+
channelAgent;
|
|
19
|
+
docParser = createDiapDocParser();
|
|
20
|
+
judgmentEngine = createChannelJudgmentEngine();
|
|
21
|
+
discoveredPeers = new Map();
|
|
22
|
+
peerDialogueHistory = new Map();
|
|
23
|
+
heartbeatInterval = null;
|
|
24
|
+
dialogueInterval = null;
|
|
25
|
+
isRunning = false;
|
|
26
|
+
ipfsEndpoint;
|
|
27
|
+
ipnsName = null;
|
|
28
|
+
ownCid = null;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.config = {
|
|
31
|
+
autoDiscovery: true,
|
|
32
|
+
autoDialogue: false,
|
|
33
|
+
dialogueInterval: 60 * 1000, // 1 分钟
|
|
34
|
+
...config
|
|
35
|
+
};
|
|
36
|
+
this.ipfsEndpoint = config.ipfsEndpoint || 'http://127.0.0.1:5001';
|
|
37
|
+
this.ipnsName = config.ipnsName || null;
|
|
38
|
+
// 创建 Channel Agent
|
|
39
|
+
const agentConfig = {
|
|
40
|
+
name: config.name,
|
|
41
|
+
port: config.port,
|
|
42
|
+
domain: '通用',
|
|
43
|
+
capabilities: config.capabilities || ['对话', '分析', '协作']
|
|
44
|
+
};
|
|
45
|
+
this.channelAgent = createChannelAgent(agentConfig);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 启动 Agent
|
|
49
|
+
*/
|
|
50
|
+
async start() {
|
|
51
|
+
await this.channelAgent.start();
|
|
52
|
+
// 发布初始 persona 到 IPFS
|
|
53
|
+
await this.publishPersona();
|
|
54
|
+
// 启动心跳循环
|
|
55
|
+
this.startHeartbeat();
|
|
56
|
+
// 启动自动对话循环
|
|
57
|
+
if (this.config.autoDialogue) {
|
|
58
|
+
this.startAutoDialogue();
|
|
59
|
+
}
|
|
60
|
+
this.isRunning = true;
|
|
61
|
+
console.log(`[HeartbeatAgent] ${this.config.name} started`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 停止 Agent
|
|
65
|
+
*/
|
|
66
|
+
stop() {
|
|
67
|
+
this.isRunning = false;
|
|
68
|
+
if (this.heartbeatInterval) {
|
|
69
|
+
clearInterval(this.heartbeatInterval);
|
|
70
|
+
this.heartbeatInterval = null;
|
|
71
|
+
}
|
|
72
|
+
if (this.dialogueInterval) {
|
|
73
|
+
clearInterval(this.dialogueInterval);
|
|
74
|
+
this.dialogueInterval = null;
|
|
75
|
+
}
|
|
76
|
+
this.channelAgent.shutdown();
|
|
77
|
+
console.log(`[HeartbeatAgent] ${this.config.name} stopped`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 发布 persona 到 IPFS
|
|
81
|
+
*/
|
|
82
|
+
async publishPersona() {
|
|
83
|
+
try {
|
|
84
|
+
const persona = this.channelAgent.getPersona();
|
|
85
|
+
if (!persona) {
|
|
86
|
+
console.log('[HeartbeatAgent] No persona to publish');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const doc = {
|
|
90
|
+
id: this.channelAgent.getDid(),
|
|
91
|
+
name: persona.name,
|
|
92
|
+
description: persona.description,
|
|
93
|
+
capabilities: persona.capabilities,
|
|
94
|
+
interests: persona.interests,
|
|
95
|
+
personality: persona.personality,
|
|
96
|
+
greeting: persona.greeting,
|
|
97
|
+
version: '1.0',
|
|
98
|
+
updatedAt: new Date().toISOString()
|
|
99
|
+
};
|
|
100
|
+
const content = JSON.stringify(doc);
|
|
101
|
+
const { IpfsClient } = await import('@diap/sdk');
|
|
102
|
+
const ipfs = new IpfsClient(this.ipfsEndpoint, null);
|
|
103
|
+
const result = await ipfs.upload(content);
|
|
104
|
+
this.ownCid = typeof result === 'string' ? result : result?.cid;
|
|
105
|
+
if (this.ownCid) {
|
|
106
|
+
console.log(`[HeartbeatAgent] Published to IPFS: ${this.ownCid}`);
|
|
107
|
+
// 发布到 IPNS
|
|
108
|
+
if (this.ipnsName) {
|
|
109
|
+
try {
|
|
110
|
+
await ipfs.publishIpns(this.ipnsName, this.ownCid);
|
|
111
|
+
console.log(`[HeartbeatAgent] Published to IPNS: ${this.ipnsName}`);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
console.warn(`[HeartbeatAgent] IPNS publish failed:`, e);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
console.warn('[HeartbeatAgent] IPFS publish failed:', e);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 启动心跳循环
|
|
125
|
+
*/
|
|
126
|
+
startHeartbeat() {
|
|
127
|
+
this.heartbeatInterval = setInterval(async () => {
|
|
128
|
+
await this.heartbeat();
|
|
129
|
+
}, 30000); // 30 秒
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 心跳处理
|
|
133
|
+
*/
|
|
134
|
+
async heartbeat() {
|
|
135
|
+
if (!this.isRunning)
|
|
136
|
+
return;
|
|
137
|
+
try {
|
|
138
|
+
// 1. 发布/更新 persona
|
|
139
|
+
await this.publishPersona();
|
|
140
|
+
// 2. 自动发现智能体
|
|
141
|
+
if (this.config.autoDiscovery) {
|
|
142
|
+
await this.discoverPeers();
|
|
143
|
+
}
|
|
144
|
+
// 3. 解析已发现但未解析的智能体
|
|
145
|
+
await this.resolveUnresolvedPeers();
|
|
146
|
+
console.log(`[HeartbeatAgent] Beat: ${this.discoveredPeers.size} peers discovered`);
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
console.error('[HeartbeatAgent] Heartbeat error:', e);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 发现对等节点
|
|
154
|
+
*/
|
|
155
|
+
async discoverPeers() {
|
|
156
|
+
try {
|
|
157
|
+
const { p2pNetwork } = await import('../../network/p2p.js');
|
|
158
|
+
// 广播发现请求
|
|
159
|
+
const request = {
|
|
160
|
+
type: 'discovery_request',
|
|
161
|
+
requesterDid: this.channelAgent.getDid(),
|
|
162
|
+
requesterName: this.config.name,
|
|
163
|
+
capabilities: this.config.capabilities || [],
|
|
164
|
+
timestamp: Date.now()
|
|
165
|
+
};
|
|
166
|
+
await p2pNetwork.broadcast('agent_discovery', JSON.stringify(request));
|
|
167
|
+
// 处理已有的连接
|
|
168
|
+
const connectedPeers = p2pNetwork.getConnectedPeers();
|
|
169
|
+
for (const peer of connectedPeers) {
|
|
170
|
+
const did = peer.peerId ? `did:key:${peer.peerId.substring(0, 16)}` : peer.peerId;
|
|
171
|
+
if (!this.discoveredPeers.has(did)) {
|
|
172
|
+
this.discoveredPeers.set(did, {
|
|
173
|
+
did,
|
|
174
|
+
name: `Peer-${did.substring(8, 16)}`,
|
|
175
|
+
peerId: peer.peerId,
|
|
176
|
+
capabilities: [],
|
|
177
|
+
lastSeen: Date.now()
|
|
178
|
+
});
|
|
179
|
+
console.log(`[HeartbeatAgent] Discovered peer: ${did}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
console.warn('[HeartbeatAgent] Peer discovery failed:', e);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 解析未解析的智能体 DiapDoc
|
|
189
|
+
*/
|
|
190
|
+
async resolveUnresolvedPeers() {
|
|
191
|
+
for (const [did, peer] of this.discoveredPeers) {
|
|
192
|
+
if (peer.doc)
|
|
193
|
+
continue; // 已解析
|
|
194
|
+
try {
|
|
195
|
+
// 如果有 IPNSName,尝试解析
|
|
196
|
+
// 这里简化处理,实际应该从发现响应中获取 IPNSName
|
|
197
|
+
// 或者通过 peerId 查询对方的 DiapDoc
|
|
198
|
+
// 标记为已处理,避免重复解析
|
|
199
|
+
peer.lastSeen = Date.now();
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
console.warn(`[HeartbeatAgent] Failed to resolve ${did}:`, e);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 处理发现的智能体公告
|
|
208
|
+
*/
|
|
209
|
+
handleChannelAnnouncement(announcement) {
|
|
210
|
+
const did = announcement.leaderDid;
|
|
211
|
+
// 检查是否已发现
|
|
212
|
+
if (this.discoveredPeers.has(did)) {
|
|
213
|
+
const peer = this.discoveredPeers.get(did);
|
|
214
|
+
peer.lastSeen = Date.now();
|
|
215
|
+
if (announcement.capabilities) {
|
|
216
|
+
peer.capabilities = announcement.capabilities;
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// 新发现
|
|
221
|
+
const peer = {
|
|
222
|
+
did,
|
|
223
|
+
name: announcement.channelName,
|
|
224
|
+
channelId: announcement.channelId,
|
|
225
|
+
capabilities: announcement.capabilities || [],
|
|
226
|
+
lastSeen: Date.now()
|
|
227
|
+
};
|
|
228
|
+
this.discoveredPeers.set(did, peer);
|
|
229
|
+
console.log(`[HeartbeatAgent] New peer from announcement: ${announcement.channelName} (${did})`);
|
|
230
|
+
// 如果有 CID,尝试解析 DiapDoc
|
|
231
|
+
if (announcement.cid) {
|
|
232
|
+
this.resolvePeerFromCID(did, announcement.cid);
|
|
233
|
+
}
|
|
234
|
+
else if (announcement.ipnsName) {
|
|
235
|
+
this.resolvePeerFromIPNS(did, announcement.ipnsName);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* 从 CID 解析对等节点 DiapDoc
|
|
240
|
+
*/
|
|
241
|
+
async resolvePeerFromCID(did, cid) {
|
|
242
|
+
try {
|
|
243
|
+
const result = await this.docParser.parseFromCID(cid, this.ipfsEndpoint);
|
|
244
|
+
if (result.success && result.doc) {
|
|
245
|
+
const peer = this.discoveredPeers.get(did);
|
|
246
|
+
if (peer) {
|
|
247
|
+
peer.doc = result.doc;
|
|
248
|
+
peer.name = result.doc.name;
|
|
249
|
+
peer.capabilities = result.doc.capabilities;
|
|
250
|
+
console.log(`[HeartbeatAgent] Resolved DiapDoc for ${result.doc.name}: ${result.doc.capabilities.join(', ')}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (e) {
|
|
255
|
+
console.warn(`[HeartbeatAgent] Failed to resolve CID ${cid}:`, e);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 从 IPNS 解析对等节点 DiapDoc
|
|
260
|
+
*/
|
|
261
|
+
async resolvePeerFromIPNS(did, ipnsName) {
|
|
262
|
+
try {
|
|
263
|
+
const result = await this.docParser.parseFromIPNS(ipnsName, this.ipfsEndpoint);
|
|
264
|
+
if (result.success && result.doc) {
|
|
265
|
+
const peer = this.discoveredPeers.get(did);
|
|
266
|
+
if (peer) {
|
|
267
|
+
peer.doc = result.doc;
|
|
268
|
+
peer.name = result.doc.name;
|
|
269
|
+
peer.capabilities = result.doc.capabilities;
|
|
270
|
+
console.log(`[HeartbeatAgent] Resolved DiapDoc via IPNS for ${result.doc.name}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (e) {
|
|
275
|
+
console.warn(`[HeartbeatAgent] Failed to resolve IPNS ${ipnsName}:`, e);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 启动自动对话循环
|
|
280
|
+
*/
|
|
281
|
+
startAutoDialogue() {
|
|
282
|
+
this.dialogueInterval = setInterval(async () => {
|
|
283
|
+
await this.processAutoDialogue();
|
|
284
|
+
}, this.config.dialogueInterval);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* 处理自动对话
|
|
288
|
+
*/
|
|
289
|
+
async processAutoDialogue() {
|
|
290
|
+
if (!this.isRunning)
|
|
291
|
+
return;
|
|
292
|
+
for (const [did, peer] of this.discoveredPeers) {
|
|
293
|
+
// 检查是否应该触发对话
|
|
294
|
+
if (!this.shouldTriggerDialogue(peer))
|
|
295
|
+
continue;
|
|
296
|
+
// 生成自动消息
|
|
297
|
+
const message = this.generateAutoMessage(peer);
|
|
298
|
+
if (!message)
|
|
299
|
+
continue;
|
|
300
|
+
// 获取对话历史
|
|
301
|
+
const history = this.peerDialogueHistory.get(did) || [];
|
|
302
|
+
const historyMessages = history.map(e => e.message);
|
|
303
|
+
// 判断是否需要调用 Harness
|
|
304
|
+
const context = {
|
|
305
|
+
conversationHistory: historyMessages,
|
|
306
|
+
currentMessage: message,
|
|
307
|
+
senderName: peer.name
|
|
308
|
+
};
|
|
309
|
+
const decision = this.judgmentEngine.decide(context);
|
|
310
|
+
if (decision.shouldCall) {
|
|
311
|
+
console.log(`[HeartbeatAgent] Auto-triggering Harness: Gate ${decision.gate} for ${peer.name}`);
|
|
312
|
+
// 这里可以发送消息触发对方响应
|
|
313
|
+
// 或者直接记录到历史
|
|
314
|
+
}
|
|
315
|
+
peer.lastDialogue = Date.now();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* 判断是否应该触发对话
|
|
320
|
+
*/
|
|
321
|
+
shouldTriggerDialogue(peer) {
|
|
322
|
+
// 刚发现的智能体不立即对话
|
|
323
|
+
if (Date.now() - peer.lastSeen < 60000)
|
|
324
|
+
return false;
|
|
325
|
+
// 频繁对话的智能体减少频率
|
|
326
|
+
if (peer.lastDialogue && Date.now() - peer.lastDialogue < 300000)
|
|
327
|
+
return false;
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* 生成自动消息
|
|
332
|
+
*/
|
|
333
|
+
generateAutoMessage(peer) {
|
|
334
|
+
const greetings = [
|
|
335
|
+
`你好 ${peer.name},我是 ${this.config.name}。`,
|
|
336
|
+
`你好!我注意到你也有 ${peer.capabilities[0] || '协作'} 能力。`,
|
|
337
|
+
`你好!我们有共同的兴趣领域。`
|
|
338
|
+
];
|
|
339
|
+
const topicMessages = [
|
|
340
|
+
'你最近在做什么项目?',
|
|
341
|
+
'你对哪些话题感兴趣?',
|
|
342
|
+
'有什么我可以帮忙的吗?'
|
|
343
|
+
];
|
|
344
|
+
// 首次对话使用问候
|
|
345
|
+
if (!peer.lastDialogue) {
|
|
346
|
+
return greetings[Math.floor(Math.random() * greetings.length)];
|
|
347
|
+
}
|
|
348
|
+
// 后续对话使用主题
|
|
349
|
+
return topicMessages[Math.floor(Math.random() * topicMessages.length)];
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* 向指定对等节点发送消息
|
|
353
|
+
*/
|
|
354
|
+
async sendMessage(targetDid, content) {
|
|
355
|
+
const peer = this.discoveredPeers.get(targetDid);
|
|
356
|
+
if (!peer) {
|
|
357
|
+
return { success: false };
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
// 如果有 P2P 连接,通过 P2P 发送
|
|
361
|
+
if (peer.peerId) {
|
|
362
|
+
const { p2pNetwork } = await import('../../network/p2p.js');
|
|
363
|
+
const payload = JSON.stringify({
|
|
364
|
+
from: this.channelAgent.getDid(),
|
|
365
|
+
fromName: this.config.name,
|
|
366
|
+
content,
|
|
367
|
+
timestamp: Date.now()
|
|
368
|
+
});
|
|
369
|
+
await p2pNetwork.sendMessage(peer.peerId, 'agent_message', payload);
|
|
370
|
+
}
|
|
371
|
+
// 记录到历史
|
|
372
|
+
const history = this.peerDialogueHistory.get(targetDid) || [];
|
|
373
|
+
history.push({
|
|
374
|
+
id: `msg_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
|
375
|
+
speaker: this.config.name,
|
|
376
|
+
message: content,
|
|
377
|
+
timestamp: Date.now(),
|
|
378
|
+
harnessCalled: false
|
|
379
|
+
});
|
|
380
|
+
this.peerDialogueHistory.set(targetDid, history);
|
|
381
|
+
return { success: true };
|
|
382
|
+
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
console.warn(`[HeartbeatAgent] Failed to send message to ${targetDid}:`, e);
|
|
385
|
+
return { success: false };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* 处理收到的消息
|
|
390
|
+
*/
|
|
391
|
+
async handleMessage(fromDid, fromName, content) {
|
|
392
|
+
// 获取对话历史
|
|
393
|
+
const history = this.peerDialogueHistory.get(fromDid) || [];
|
|
394
|
+
const historyMessages = history.map(e => e.message);
|
|
395
|
+
// 通过 Channel Agent 处理
|
|
396
|
+
const result = await this.channelAgent.receiveMessage(fromName, content);
|
|
397
|
+
// 记录到历史
|
|
398
|
+
history.push({
|
|
399
|
+
id: `msg_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
|
400
|
+
speaker: fromName,
|
|
401
|
+
message: content,
|
|
402
|
+
timestamp: Date.now(),
|
|
403
|
+
harnessCalled: result.harnessCalled,
|
|
404
|
+
gate: result.gate
|
|
405
|
+
});
|
|
406
|
+
history.push({
|
|
407
|
+
id: `msg_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
|
408
|
+
speaker: this.config.name,
|
|
409
|
+
message: result.response,
|
|
410
|
+
timestamp: Date.now(),
|
|
411
|
+
harnessCalled: result.harnessCalled,
|
|
412
|
+
gate: result.gate
|
|
413
|
+
});
|
|
414
|
+
this.peerDialogueHistory.set(fromDid, history.slice(-50)); // 保留最近 50 条
|
|
415
|
+
// 更新对等节点活跃时间
|
|
416
|
+
const peer = this.discoveredPeers.get(fromDid);
|
|
417
|
+
if (peer) {
|
|
418
|
+
peer.lastSeen = Date.now();
|
|
419
|
+
peer.lastDialogue = Date.now();
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
// ==================== Getters ====================
|
|
424
|
+
getName() {
|
|
425
|
+
return this.config.name;
|
|
426
|
+
}
|
|
427
|
+
getDid() {
|
|
428
|
+
return this.channelAgent.getDid();
|
|
429
|
+
}
|
|
430
|
+
getDiscoveredPeers() {
|
|
431
|
+
return Array.from(this.discoveredPeers.values());
|
|
432
|
+
}
|
|
433
|
+
getPeerDialogueHistory(did) {
|
|
434
|
+
return this.peerDialogueHistory.get(did) || [];
|
|
435
|
+
}
|
|
436
|
+
isActive() {
|
|
437
|
+
return this.isRunning;
|
|
438
|
+
}
|
|
439
|
+
getOwnCID() {
|
|
440
|
+
return this.ownCid;
|
|
441
|
+
}
|
|
442
|
+
getOwnIPNS() {
|
|
443
|
+
return this.ipnsName;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// 工厂函数
|
|
447
|
+
export function createHeartbeatAgent(config) {
|
|
448
|
+
return new ChannelHeartbeatAgent(config);
|
|
449
|
+
}
|
|
450
|
+
// 全局注册表
|
|
451
|
+
export class HeartbeatAgentRegistry {
|
|
452
|
+
agents = new Map();
|
|
453
|
+
register(agent) {
|
|
454
|
+
this.agents.set(agent.getDid(), agent);
|
|
455
|
+
}
|
|
456
|
+
unregister(did) {
|
|
457
|
+
const agent = this.agents.get(did);
|
|
458
|
+
if (agent) {
|
|
459
|
+
agent.stop();
|
|
460
|
+
this.agents.delete(did);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
get(did) {
|
|
464
|
+
return this.agents.get(did);
|
|
465
|
+
}
|
|
466
|
+
getByName(name) {
|
|
467
|
+
for (const agent of this.agents.values()) {
|
|
468
|
+
if (agent.getName() === name) {
|
|
469
|
+
return agent;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
list() {
|
|
475
|
+
return Array.from(this.agents.values());
|
|
476
|
+
}
|
|
477
|
+
broadcastToAll(type, payload) {
|
|
478
|
+
let count = 0;
|
|
479
|
+
for (const agent of this.agents.values()) {
|
|
480
|
+
const peers = agent.getDiscoveredPeers();
|
|
481
|
+
for (const peer of peers) {
|
|
482
|
+
agent.sendMessage(peer.did, payload);
|
|
483
|
+
count++;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return count;
|
|
487
|
+
}
|
|
488
|
+
clear() {
|
|
489
|
+
for (const agent of this.agents.values()) {
|
|
490
|
+
agent.stop();
|
|
491
|
+
}
|
|
492
|
+
this.agents.clear();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DiapDoc Parser - 解析 Diap 去中心化身份协议文档
|
|
3
|
+
*
|
|
4
|
+
* 负责解析 Diap 智能体发布的 IPFS/IPNS 文档,提取:
|
|
5
|
+
* - DID (去中心化身份标识)
|
|
6
|
+
* - 频道信息
|
|
7
|
+
* - 能力列表
|
|
8
|
+
* - 网络地址
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* DiapDoc Parser 类
|
|
12
|
+
*/
|
|
13
|
+
export class DiapDocParser {
|
|
14
|
+
/**
|
|
15
|
+
* 解析 Diap 文档 (从 IPFS CID 或 IPNS 获取的内容)
|
|
16
|
+
*/
|
|
17
|
+
parse(content) {
|
|
18
|
+
try {
|
|
19
|
+
const data = JSON.parse(content);
|
|
20
|
+
// 验证必需字段
|
|
21
|
+
if (!data.id && !data.did) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
error: 'Missing required field: id or did'
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const doc = {
|
|
28
|
+
id: data.id || data.did,
|
|
29
|
+
name: data.name || 'Unknown Agent',
|
|
30
|
+
version: data.version || '1.0',
|
|
31
|
+
// 能力信息
|
|
32
|
+
capabilities: this.normalizeArray(data.capabilities || data.capability || []),
|
|
33
|
+
interests: this.normalizeArray(data.interests || data.interest || []),
|
|
34
|
+
// 网络信息
|
|
35
|
+
peerId: data.peerId || data.peer_id || data.peer,
|
|
36
|
+
multiaddrs: this.normalizeArray(data.multiaddrs || data.addresses || []),
|
|
37
|
+
relayAddr: data.relayAddr || data.relay_addr || data.relay,
|
|
38
|
+
// 频道信息
|
|
39
|
+
channels: this.parseChannels(data.channels || data.channel || []),
|
|
40
|
+
// 身份信息
|
|
41
|
+
publicKey: data.publicKey || data.public_key,
|
|
42
|
+
signature: data.signature,
|
|
43
|
+
// 时间戳
|
|
44
|
+
createdAt: data.createdAt || data.created_at,
|
|
45
|
+
updatedAt: data.updatedAt || data.updated_at,
|
|
46
|
+
timestamp: data.timestamp || data.createdAt ? Date.now() : undefined
|
|
47
|
+
};
|
|
48
|
+
return { success: true, doc };
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return {
|
|
52
|
+
success: false,
|
|
53
|
+
error: `Failed to parse DiapDoc: ${e}`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 解析频道数组
|
|
59
|
+
*/
|
|
60
|
+
parseChannels(channels) {
|
|
61
|
+
if (!Array.isArray(channels))
|
|
62
|
+
return [];
|
|
63
|
+
return channels.map(ch => {
|
|
64
|
+
if (typeof ch === 'string') {
|
|
65
|
+
return {
|
|
66
|
+
id: ch,
|
|
67
|
+
name: ch
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
id: ch.id || ch.channelId || ch.channel_id || String(Date.now()),
|
|
72
|
+
name: ch.name || ch.channelName || ch.channel_name || 'Unnamed Channel',
|
|
73
|
+
topic: ch.topic,
|
|
74
|
+
type: ch.type,
|
|
75
|
+
isPublic: ch.isPublic ?? ch.is_public ?? true,
|
|
76
|
+
memberCount: ch.memberCount || ch.member_count
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 规范化数组
|
|
82
|
+
*/
|
|
83
|
+
normalizeArray(value) {
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
return value.map(v => String(v));
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === 'string') {
|
|
88
|
+
return [value];
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 从 CID 解析文档
|
|
94
|
+
*/
|
|
95
|
+
async parseFromCID(cid, ipfsEndpoint = 'http://127.0.0.1:5001') {
|
|
96
|
+
try {
|
|
97
|
+
const { IpfsClient } = await import('@diap/sdk');
|
|
98
|
+
const ipfs = new IpfsClient(ipfsEndpoint, null);
|
|
99
|
+
const content = await ipfs.get(cid);
|
|
100
|
+
return this.parse(content);
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
error: `Failed to fetch from IPFS: ${e}`
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 从 IPNS 解析文档
|
|
111
|
+
*/
|
|
112
|
+
async parseFromIPNS(ipnsName, ipfsEndpoint = 'http://127.0.0.1:5001') {
|
|
113
|
+
try {
|
|
114
|
+
const { IpfsClient } = await import('@diap/sdk');
|
|
115
|
+
const ipfs = new IpfsClient(ipfsEndpoint, null);
|
|
116
|
+
const cid = await ipfs.resolveIpns(ipnsName);
|
|
117
|
+
if (!cid) {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
error: 'IPNS resolve returned no CID'
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return this.parseFromCID(cid, ipfsEndpoint);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
error: `Failed to resolve IPNS: ${e}`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* 从 URL 解析 (用于解析网关返回的内容)
|
|
134
|
+
*/
|
|
135
|
+
parseFromUrl(url) {
|
|
136
|
+
return fetch(url)
|
|
137
|
+
.then(resp => resp.text())
|
|
138
|
+
.then(content => this.parse(content))
|
|
139
|
+
.catch(e => ({
|
|
140
|
+
success: false,
|
|
141
|
+
error: `Failed to fetch URL: ${e}`
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 提取 DID
|
|
146
|
+
*/
|
|
147
|
+
extractDID(doc) {
|
|
148
|
+
return doc.id;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 提取用于连接的信息
|
|
152
|
+
*/
|
|
153
|
+
extractConnectionInfo(doc) {
|
|
154
|
+
return {
|
|
155
|
+
peerId: doc.peerId,
|
|
156
|
+
multiaddrs: doc.multiaddrs || [],
|
|
157
|
+
relayAddr: doc.relayAddr
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 提取频道列表
|
|
162
|
+
*/
|
|
163
|
+
extractChannels(doc) {
|
|
164
|
+
return doc.channels || [];
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 验证文档签名 (如果实现)
|
|
168
|
+
*/
|
|
169
|
+
async verifySignature(doc) {
|
|
170
|
+
if (!doc.publicKey || !doc.signature) {
|
|
171
|
+
return true; // 无签名时默认通过
|
|
172
|
+
}
|
|
173
|
+
// TODO: 实现签名验证
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// 全局解析器实例
|
|
178
|
+
let parserInstance = null;
|
|
179
|
+
export function createDiapDocParser() {
|
|
180
|
+
return new DiapDocParser();
|
|
181
|
+
}
|
|
182
|
+
export function getDiapDocParser() {
|
|
183
|
+
if (!parserInstance) {
|
|
184
|
+
parserInstance = new DiapDocParser();
|
|
185
|
+
}
|
|
186
|
+
return parserInstance;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 从 DiapAnnouncement 提取 DiapDoc 信息
|
|
190
|
+
*/
|
|
191
|
+
export function extractDocFromAnnouncement(announcement) {
|
|
192
|
+
return {
|
|
193
|
+
id: announcement.leaderDid,
|
|
194
|
+
name: announcement.channelName,
|
|
195
|
+
version: '1.0',
|
|
196
|
+
capabilities: announcement.capabilities || [],
|
|
197
|
+
interests: announcement.interests || [],
|
|
198
|
+
channels: announcement.channelId ? [{
|
|
199
|
+
id: announcement.channelId,
|
|
200
|
+
name: announcement.channelName,
|
|
201
|
+
topic: announcement.topic
|
|
202
|
+
}] : []
|
|
203
|
+
};
|
|
204
|
+
}
|