// fmodeagent-deliverables v0.0.2 — 智能体任务成果上报与聚合 (表: AgentDeliverable) // 调用: POST /api/functions {token, id:, params:{action:'report'|'list'|'byProject', ...}} // v0.0.2 变更: // 1. report 移除 FMODE_AGENT_SUPERADMIN 检查 → 仅校验「该 agent 的 owner」;agent 无 owner 时首次上报自动绑定 // 2. agent 不存在时自动注册(自注册,agentId 有效即可) // 3. agentId / agentObjectId 任一可传,自动补全,并写入 agent 指针 // 4. list/byProject 可见性保持 v0.0.1 逻辑(超管全量 | 有公司看本公司 | 无公司看自己) var _sessionToken = function () { if (params && params.token) return params.token; if (request.body && request.body.token) return request.body.token; return null; }; var _getUser = async function (t) { if (!t) return null; if (request.user) return request.user; try { var q = new Parse.Query(Parse.Session); q.equalTo('sessionToken', t); q.include('user'); var s = await q.first({ useMasterKey: true }); return s ? s.get('user') : null; } catch (e) { return null; } }; var _isSuper = async function (user) { try { var q = new Parse.Query(Parse.Role); q.equalTo('name', 'FMODE_AGENT_SUPERADMIN'); q.equalTo('users', user); return (await q.first({ useMasterKey: true })) != null; } catch (e) { return false; } }; var _idOf = function (v) { if (!v) return null; if (typeof v === 'string') return v; return v.id || null; }; var UPDATABLE = ['agentId','title','summary','project','projectName','artifacts','tags','deliveredAt','status','extra']; function handler(request, response) { (async function () { try { var p = params || {}; var user = await _getUser(_sessionToken()); if (!user) return _respond(response, 401, { code: 401, msg: 'SessionToken 无效' }); var act = p.action || 'list'; var superAd = await _isSuper(user); var uCompany = user.get('company') || null; if (act === 'report') { if (!p.title) return _respond(response, 400, { code: 400, msg: 'title required' }); if (!p.agentId && !p.agentObjectId) return _respond(response, 400, { code: 400, msg: 'agentId/agentObjectId required' }); // ---- 解析 agent:先按 objectId,再按 agentId;都没有则自注册 ---- var agent = null; if (p.agentObjectId) { var q0 = new Parse.Query('FmodeAgent'); q0.equalTo('objectId', p.agentObjectId); agent = await q0.first({ useMasterKey: true }); } if (!agent && p.agentId) { var q1 = new Parse.Query('FmodeAgent'); q1.equalTo('agentId', p.agentId); agent = await q1.first({ useMasterKey: true }); } if (!agent) { if (!p.agentId) return _respond(response, 400, { code: 400, msg: '新 agent 注册需要 agentId' }); agent = new Parse.Object('FmodeAgent'); agent.set('agentId', p.agentId); agent.set('user', user); agent.set('status', 'online'); agent.set('heartbeatAt', new Date()); agent = await agent.save(null, { useMasterKey: true }); } // ---- 鉴权:仅该 agent 的 owner(v0.0.2 移除超管代报)---- var agentUserId = _idOf(agent.get('user')); if (agentUserId && agentUserId !== user.id) { return _respond(response, 403, { code: 403, msg: '仅该容器的 owner 可上报' }); } if (!agentUserId) { agent.set('user', user); await agent.save(null, { useMasterKey: true }); agentUserId = user.id; } // ---- 写交付物 ---- var obj = new Parse.Object('AgentDeliverable'); for (var i = 0; i < UPDATABLE.length; i++) { var k = UPDATABLE[i]; if (p[k] === undefined) continue; if (k === 'deliveredAt') obj.set('deliveredAt', new Date(p[k])); else obj.set(k, p[k]); } obj.set('agentId', agent.get('agentId') || p.agentId); obj.set('agent', agent); obj.set('agentObjectId', agent.id); obj.set('user', user); if (agent.get('company')) obj.set('company', agent.get('company')); obj.set('reportedAt', new Date()); if (p.deliveredAt === undefined) obj.set('deliveredAt', new Date()); if (!p.status) obj.set('status', 'delivered'); var acl = new Parse.ACL(); acl.setReadAccess(user.id, true); acl.setWriteAccess(user.id, true); acl.setPublicReadAccess(false); acl.setPublicWriteAccess(false); obj.setACL(acl); var saved = await obj.save(null, { useMasterKey: true }); // ---- 更新 agent 心跳 ---- agent.set('heartbeatAt', new Date()); agent.set('status', 'online'); await agent.save(null, { useMasterKey: true }); return _respond(response, 200, { code: 200, id: saved.id, agentId: obj.get('agentId'), agentObjectId: agent.id, title: p.title }); } // ---- list / byProject(沿用 v0.0.1)---- var q = new Parse.Query('AgentDeliverable'); if (p.agentId) q.equalTo('agentId', p.agentId); if (p.project) q.equalTo('project', p.project); if (!superAd) { if (uCompany) { q.equalTo('company', uCompany); } else { q.equalTo('user', user); } } if (p.before) q.lessThan('deliveredAt', new Date(p.before)); q.descending('deliveredAt'); q.limit(Math.min(p.limit || 50, 200)); var rows = await q.find({ useMasterKey: true }); var items = rows.map(function (r) { return { id: r.id, agentId: r.get('agentId'), title: r.get('title'), summary: r.get('summary'), project: r.get('project'), projectName: r.get('projectName'), artifacts: r.get('artifacts') || [], tags: r.get('tags') || [], deliveredAt: r.get('deliveredAt'), status: r.get('status') || 'delivered', extra: r.get('extra') }; }); if (act === 'byProject') { var groups = {}; for (var j = 0; j < items.length; j++) { var it = items[j]; var key = it.project || '未归类'; if (!groups[key]) groups[key] = { project: key, projectName: it.projectName || key, count: 0, items: [] }; groups[key].count++; groups[key].items.push(it); } return _respond(response, 200, { code: 200, groups: Object.keys(groups).map(function (k2) { return groups[k2]; }) }); } return _respond(response, 200, { code: 200, count: items.length, deliverables: items }); } catch (e) { return _respond(response, 500, { code: 500, msg: String(e && e.message || e) }); } })(); } function _respond(response, status, body) { if (response && typeof response.status === 'function') { response.status(status); response.json(body); } else if (response && typeof response.success === 'function') { status === 200 ? response.success(body) : response.error(body); } return body; }