|
|
@@ -0,0 +1,145 @@
|
|
|
+// fmodeagent-deliverables v0.0.2 — 智能体任务成果上报与聚合 (表: AgentDeliverable)
|
|
|
+// 调用: POST /api/functions {token, id:<Function objectId>, 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;
|
|
|
+}
|