Просмотр исходного кода

feat: skill-deliverable v0.0.1 交付物自动上报技能

- CLI/SDK: resolveAgentId()+resolveSessionToken()+report()
- 自主身份解析: FMODE_AGENT_ID > AGENT_ID > config
- sessionToken: env > user.json > config
- 云函数 v2: 移除超管检查,仅 owner 校验,首次自动注册
- docs/deployment-guide.md: 运维部署必检清单
liuyuyang 2 недель назад
Сommit
14cf9be041
9 измененных файлов с 557 добавлено и 0 удалено
  1. 3 0
      .gitignore
  2. 2 0
      LICENSE
  3. 49 0
      README.md
  4. 118 0
      SKILL.md
  5. 70 0
      bin/skill-deliverable.mjs
  6. 116 0
      docs/deployment-guide.md
  7. 115 0
      lib/index.mjs
  8. 24 0
      package.json
  9. 60 0
      skills/skill-deliverable/SKILL.md

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+node_modules
+.env
+*.log

+ 2 - 0
LICENSE

@@ -0,0 +1,2 @@
+MIT License
+Copyright (c) 2026 Fmode

+ 49 - 0
README.md

@@ -0,0 +1,49 @@
+# skill-deliverable
+
+系统级技能:**交付物自动上报引擎**。每台容器自主解析身份+凭据,调平台云函数将交付物入库。
+
+## 一句话
+
+> 每次你干完活,它自动向后台报告「我做了什么、产出了什么」。
+
+## 安装
+
+```bash
+npm install -g skill-deliverable
+# 或
+git clone https://git.fmode.cn/fmode/skill-deliverable.git
+```
+
+## CLI
+
+```bash
+# 检查本容器身份+token 是否可上报
+skill-deliverable check
+
+# 上报一个交付物
+skill-deliverable report --title "某任务" --summary "干了些啥" --project "项目名"
+
+# 查看近期上报
+skill-deliverable list
+```
+
+## SDK
+
+```javascript
+import { report } from 'skill-deliverable';
+const r = await report({
+  agentId: 'agent-node',
+  title: 'RSI 课程长页',
+  summary: '进化工程方法论版',
+  artifacts: [{ name: 'HTML', url: 'https://...', type: 'html' }],
+});
+```
+
+## 文档
+
+- [`SKILL.md`](SKILL.md) — 完整技能规范
+- [`docs/deployment-guide.md`](docs/deployment-guide.md) — 运维部署指南
+
+## License
+
+MIT

+ 118 - 0
SKILL.md

@@ -0,0 +1,118 @@
+---
+slug: skill-deliverable
+displayName: skill-deliverable
+version: 0.0.1
+summary: 系统级·交付物自动上报。自主解析 agentId → objectId + sessionToken → 调云函数 → 入库。
+tags: [system, deliverable, reporting, agent, bootstrap]
+---
+
+# skill-deliverable — 交付物自动上报
+
+> **系统级技能** · 每台容器的自主上报引擎
+>
+> 核心能力:自主解析本容器身份(agentId/objectId)和凭据(sessionToken),调平台云函数
+> 将交付物写入 `AgentDeliverable` 表。**无需超管角色,每个容器只报自己的活。**
+
+---
+
+## 一、架构
+
+```
+每台容器 (agent-node / agent-node-xinting / ...)
+       │
+       ├─ skill-deliverable CLI/SDK
+       │    ├─ resolveAgentId()        → agentId (语义名)
+       │    ├─ resolveAgentObjectId()  → objectId (云函数自动补全/创建)
+       │    ├─ resolveSessionToken()   → 当前用户凭据
+       │    └─ report()                → POST 云函数
+       │
+       └─ FMODE 平台云函数 (lfYlgU7SkK)
+            ├─ 鉴权:sessionToken → _User → 查是否 agent 的 owner
+            ├─ 自动创造:首次上报自动创建 FmodeAgent 记录
+            ├─ 自动补全:agentId ↔ objectId 互相解析
+            └─ 写入:AgentDeliverable 表 (user/agent 双指针)
+```
+
+## 二、安装
+
+```bash
+# npm 安装
+npm install -g skill-deliverable
+
+# 或 Hermes 技能加载
+hermes skill install skill-deliverable
+
+# 或直接克隆
+git clone https://git.fmode.cn/fmode/skill-deliverable.git
+```
+
+## 三、CLI 用法
+
+```bash
+# 1. 凭据检查(验证本容器身份+token是否可用)
+skill-deliverable check
+# 输出:agentId=agent-node ✓ | sessionToken=✓ | agentObjectId=auto | status=ready
+
+# 2. 上报交付物
+skill-deliverable report --title "RSI 课程长页 v3" \
+  --summary "进化工程 RSI 方法论版课程介绍长页" \
+  --project skill-present \
+  --artifacts '{"name":"RSI长页","url":"https://s3.fmode.cn/...","type":"html"}' \
+  --tags 'course,landing'
+
+# 3. 查看本容器已有的交付物
+skill-deliverable list [--limit 5]
+
+# 4. 初始化凭证
+skill-deliverable init --mobile 186xxxxxx
+```
+
+## 四、SDK(在 Hermes 技能或 CC 中引用)
+
+```javascript
+import { resolveAgentId, resolveSessionToken, report } from 'skill-deliverable';
+
+// 解析身份+凭据
+const agentId = await resolveAgentId();
+const token = await resolveSessionToken();
+
+// 上报
+const result = await report({
+  agentId,
+  title: '任务标题',
+  summary: '一句话描述',
+  project: '项目名',
+  artifacts: [{ name: '文件', url: 'https://...', type: 'html' }],
+  tags: ['feature'],
+});
+// → { code:200, id:"abc123", agentObjectId:"xyz..." }
+```
+
+## 五、云函数 v2(需部署到平台后生效)
+
+见 `cloud/deliverable-report-v2.js`。变更说明:
+- 移除 `FMODE_AGENT_SUPERADMIN` 角色检查
+- 鉴权改为:**用户必须是该 FmodeAgent 记录的 owner**
+- 新增:首次上报自动创建 FmodeAgent 记录
+- 新增:agentId ↔ agentObjectId 自动互解析
+- 新增:自动绑定 agent 的 user 指针
+
+## 六、依赖
+
+- Node ≥ 18
+- 平台云函数 lfYlgU7SkK(v2 部署后)
+- `~/.fmode/config/user.json` 或 `FMODE_SESSION_TOKEN` 环境变量
+
+## 七、自举核查清单
+
+```
+□ 1. check() 成功 → 显示 agentId + sessionToken ✓
+□ 2. report() 成功 → 返回 objectId
+□ 3. list() 能查到刚上报的记录
+□ 4. 容器重建后仍然正常工作(agentId 不依赖 hostname)
+□ 5. 权限:非owner 调 report() → 403
+```
+
+## License
+
+MIT

+ 70 - 0
bin/skill-deliverable.mjs

@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+/**
+ * skill-deliverable CLI — 交付物自动上报
+ * Usage:
+ *   skill-deliverable check        # 检查容器身份+凭据
+ *   skill-deliverable report ...   # 上报交付物
+ *   skill-deliverable list         # 列出上报记录
+ *   skill-deliverable init         # 初始化凭据
+ */
+import { resolveAgentId, resolveSessionToken, report, list } from '../lib/index.mjs';
+
+const [cmd, ...args] = process.argv.slice(2);
+
+switch (cmd) {
+  case 'check': {
+    const agentId = resolveAgentId();
+    const token = resolveSessionToken();
+    console.log(JSON.stringify({
+      agentId: agentId || '(缺失)',
+      sessionToken: token ? token.slice(0, 12) + '...' : '(缺失)',
+      status: (agentId && token) ? 'ready' : 'incomplete',
+      hostname: require('os').hostname(),
+    }, null, 2));
+    break;
+  }
+  case 'report': {
+    const title = getArg('--title');
+    const summary = getArg('--summary') || '';
+    const project = getArg('--project') || '';
+    const artifacts = parseArg('--artifacts') || [];
+    const tags = parseArg('--tags') || [];
+    if (!title) { console.error('--title 必填'); process.exit(1); }
+    const r = await report({ agentId: resolveAgentId(), title, summary, project, artifacts, tags });
+    console.log(JSON.stringify(r, null, 2));
+    break;
+  }
+  case 'list': {
+    const limit = parseInt(getArg('--limit') || '5', 10);
+    const items = await list({ limit });
+    console.log(JSON.stringify(items, null, 2));
+    break;
+  }
+  case 'init': {
+    console.log('→ 凭据自举:尝试从 ~/.fmode/ 或 env 读取...');
+    const token = resolveSessionToken();
+    if (token) {
+      console.log('✅ 已有 sessionToken,无需初始化');
+    } else {
+      console.error('❌ 无 sessionToken。请设置 FMODE_SESSION_TOKEN 或登录 FMODE Studio');
+      process.exit(1);
+    }
+    break;
+  }
+  default:
+    console.log(`用法: skill-deliverable <check|report|list|init>
+  check          检查容器身份+凭据状态
+  report         上报交付物 (--title <必填> --summary --project --artifacts --tags)
+  list           最近上报记录 (--limit)
+  init           初始化凭据`);
+}
+
+function getArg(name) {
+  const i = args.indexOf(name);
+  return i >= 0 ? args[i + 1] : undefined;
+}
+function parseArg(name) {
+  const v = getArg(name);
+  if (!v) return undefined;
+  try { return JSON.parse(v); } catch { return v.split(','); }
+}

+ 116 - 0
docs/deployment-guide.md

@@ -0,0 +1,116 @@
+# 交付物上报技能 — 运维部署指南
+
+> 本文档写给**运维001**,确保新初始化的数字生命容器能正确上报交付物。
+> 读完这篇,你就知道每个新容器启动后该检查什么、配在哪里。
+
+---
+
+## 一、容器初始化检查清单
+
+### □ 1. 确定 AGENT_ID
+
+每台容器需要一个**语义唯一名**,格式:`{主人名}-{角色}`
+
+| 容器 | AGENT_ID 示例 |
+|------|--------------|
+| 刘雨飏 主容器 | `agent-node` |
+| 刘雨飏 雨飏001 容器 | `agent-node-tuye` |
+| 芯葶 容器 | `agent-node-xinting` |
+
+**存放位置**(优先级从高到低):
+
+| 位置 | 命令/文件 | 说明 |
+|------|-----------|------|
+| 环境变量 | `export FMODE_AGENT_ID=agent-node` | 最优先,重构建容不失 |
+| 环境变量 | `export AGENT_ID=agent-node` | 兼容旧脚本 |
+| 配置文件 | `~/.fmode/config.json` → `agentId` | 持久化,被 env 覆盖 |
+| 项目配置 | `<cwd>/.fmode/config.json` → `agentId` | 项目级覆盖 |
+
+**推荐做法**:`echo 'export FMODE_AGENT_ID=agent-node' >> ~/.bashrc`
+
+> ⚠️ **不要依赖 hostname**。Docker 容器重启后 hostname 变随机 hash(如 `6ec71e98949e`),云函数会判 `404 agent not found`。
+
+### □ 2. 确保 FMODE_SESSION_TOKEN 可被读取
+
+SessionToken 是用户登录后生成的凭据,控制 ACL(**访问权限**)。
+
+**存放位置**:
+
+| 位置 | 文件 | 说明 |
+|------|------|------|
+| 环境变量 | `FMODE_SESSION_TOKEN` | 最优 |
+| 用户配置 | `~/.fmode/config/user.json` → `sessionToken` | Hermes/Fmode Studio 自动维护 |
+| 旧配置 | `~/.fmode/config.json` → `sessionToken` | 兼容旧版本 |
+
+**推荐做法**:登录一次 FMODE Studio 后,token 自动写入 user.json。
+如 env 缺失,技能会从配置文件兜底读取。
+
+### □ 3. 验证
+
+```bash
+# 查看当前容器身份
+skill-deliverable check
+
+# 成功输出示例:
+# {
+#   "agentId": "agent-node",
+#   "sessionToken": "r:852951...",
+#   "status": "ready",
+#   "hostname": "6ec71e98949e"   // 仅显示,不用于上报
+# }
+```
+
+---
+
+## 二、FmodeStudio 容器生命周期集成
+
+在 `fmode-studio` 的 Docker 部署脚本中,新增:
+
+```bash
+# 设置 AGENT_ID
+export FMODE_AGENT_ID="agent-$(hostname -s)"  # 或用固定语义名
+
+# 确认 sessionToken 有来源
+if [ -z "$FMODE_SESSION_TOKEN" ] && [ -f "$HOME/.fmode/config/user.json" ]; then
+  export FMODE_SESSION_TOKEN=$(python3 -c "import json;print(json.load(open('$HOME/.fmode/config/user.json'))['sessionToken'])")
+fi
+
+# 运行检查
+npx --yes skill-deliverable@latest check
+```
+
+---
+
+## 三、容灾:云函数 404/403 怎么办
+
+### 「404 agent not found」
+→ FmodeAgent 表里没有这个 agentId 的记录。
+- **新容器首次上报时会自动注册**(云函数 v2 特性),等待第一次 report() 即可
+- 若等不及,用 master key 手动在 Parse `FmodeAgent` 表创建同名记录
+
+### 「403 only owner or superadmin can report」
+→ session 用户不是这个 agent 的 owner。
+- 确认该容器的报告人的 session 和该 agent 是否是同一个人
+- 云函数 v2 **已移除 superadmin 检查**,只校验 owner(用户=他自己容器的 owner)
+- 若 agent 无 owner(旧数据),首次成功上报会自动绑定
+
+---
+
+## 四、常见问题
+
+### Q: 容器重建后上报失败?
+A: 重建后 AGENT_ID 丢失 → 重设 env `FMODE_AGENT_ID`。skill-deliverable 已做 hostname 兜底但语义名丢失时 hostname 会变导致 404。**必须持久化 AGENT_ID**。
+
+### Q: 一台机器跑了多个容器怎么办?
+A: 每个容器设不同的 AGENT_ID,各自上报各自的。FmodeAgent 表以 agentId 区分。
+
+### Q: 能不能手动上报测试?
+A: 可以。`skill-deliverable report --title "测试" --project test`。失败看 `/tmp/agent-deliverable-report.log`。
+
+---
+
+## 版本记录
+
+| 版本 | 日期 | 变更 |
+|------|------|------|
+| 0.0.1 | 2026-09-23 | 初版:分离 fmode-hub 独立成技能 + 云函数 v2 改良 |

+ 115 - 0
lib/index.mjs

@@ -0,0 +1,115 @@
+/**
+ * skill-deliverable SDK — 交付物上报核心
+ * 
+ * 零依赖(使用 Node ≥ 18 内置 fetch + fs)
+ * 所有凭据从 env → config 文件 → 兜底,按高兼容链解析
+ */
+import { readFileSync, existsSync, constants } from 'fs';
+import { homedir, hostname } from 'os';
+import { join } from 'path';
+import { env } from 'process';
+
+const HOME = homedir();
+const GATEWAY = env.FMODE_API || 'https://server.fmode.cn';
+const FN_ID = env.FN_DELIVERABLES || 'lfYlgU7SkK';
+
+// ============================================================
+// Agent 身份高兼容解析
+// ============================================================
+export function resolveAgentId() {
+  // 1. env
+  if (env.FMODE_AGENT_ID) return env.FMODE_AGENT_ID;
+  if (env.AGENT_ID) return env.AGENT_ID;
+
+  // 2. ~/.fmode/config.json
+  const homeCfg = tryReadJSON(join(HOME, '.fmode', 'config.json'));
+  if (homeCfg?.agentId) return homeCfg.agentId;
+
+  // 3. ./.fmode/config.json (cwd)
+  const cwdCfg = tryReadJSON(join(process.cwd(), '.fmode', 'config.json'));
+  if (cwdCfg?.agentId) return cwdCfg.agentId;
+
+  // 4. 兜底 hostname(仅 warn)
+  const hn = hostname();
+  console.warn(`[warn] 未找到语义 agentId,回退到 hostname "${hn}"(可能不是注册名)`);
+  return hn;
+}
+
+// ============================================================
+// Session Token 高兼容解析
+// ============================================================
+export function resolveSessionToken() {
+  // 1. env
+  if (env.FMODE_SESSION_TOKEN) return env.FMODE_SESSION_TOKEN;
+
+  // 2. ~/.fmode/config/user.json
+  const userCfg = tryReadJSON(join(HOME, '.fmode', 'config', 'user.json'));
+  if (userCfg?.sessionToken) return userCfg.sessionToken;
+
+  // 3. ~/.fmode/config.json
+  const homeCfg = tryReadJSON(join(HOME, '.fmode', 'config.json'));
+  if (homeCfg?.sessionToken) return homeCfg.sessionToken;
+
+  // 4. ./.fmode/config.json (cwd)
+  const cwdCfg = tryReadJSON(join(process.cwd(), '.fmode', 'config.json'));
+  if (cwdCfg?.sessionToken) return cwdCfg.sessionToken;
+
+  return null;
+}
+
+// ============================================================
+// 云函数调用
+// ============================================================
+export async function callFn(params) {
+  const token = resolveSessionToken();
+  if (!token) throw new Error('无 sessionToken:无法调用云函数。设置 FMODE_SESSION_TOKEN 环境变量或 ~/.fmode/config/user.json');
+
+  const resp = await fetch(`${GATEWAY}/api/functions`, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify({ token, id: FN_ID, params }),
+  });
+  return resp.json();
+}
+
+// ============================================================
+// 上报交付物
+// ============================================================
+export async function report({ agentId, agentObjectId, title, summary, project, artifacts, tags }) {
+  if (!title) throw new Error('title 必填');
+  const resolvedAgentId = agentId || resolveAgentId();
+  if (!resolvedAgentId) throw new Error('agentId 必填');
+  
+  return callFn({
+    action: 'report',
+    agentId: resolvedAgentId,
+    agentObjectId: agentObjectId || '',
+    title,
+    summary: summary || '',
+    project: project || '',
+    artifacts: artifacts || [],
+    tags: tags || [],
+  });
+}
+
+// ============================================================
+// 查询交付物
+// ============================================================
+export async function list({ limit = 5, agentId } = {}) {
+  const r = await callFn({ action: 'list', agentId: agentId || resolveAgentId(), limit });
+  return r.deliverables || [];
+}
+
+// ============================================================
+// 工具
+// ============================================================
+function tryReadJSON(p) {
+  try {
+    if (!existsSync(p)) return null;
+    return JSON.parse(readFileSync(p, 'utf-8'));
+  } catch {
+    return null;
+  }
+}
+
+export default { resolveAgentId, resolveSessionToken, report, list, callFn };

+ 24 - 0
package.json

@@ -0,0 +1,24 @@
+{
+  "name": "skill-deliverable",
+  "version": "0.0.1",
+  "description": "\u7cfb\u7edf\u7ea7\u6280\u80fd\u3002\u4ea4\u4ed8\u7269\u81ea\u52a8\u4e0a\u62a5\uff1a\u81ea\u4e3b\u89e3\u6790agentId/objectId+sessionToken \u2192 \u8c03\u4e91\u51fd\u6570 \u2192 \u5165\u5e93AgentDeliverable\u3002\u6bcf\u4e2a\u5bb9\u5668\u53ea\u62a5\u81ea\u5df1\u7684\u6d3b\u3002",
+  "type": "module",
+  "bin": {
+    "skill-deliverable": "./bin/skill-deliverable.mjs"
+  },
+  "exports": {
+    "./": "./lib/index.mjs"
+  },
+  "keywords": [
+    "deliverable",
+    "reporting",
+    "fmode",
+    "agent",
+    "skill"
+  ],
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "git+https://git.fmode.cn/fmode/skill-deliverable.git"
+  }
+}

+ 60 - 0
skills/skill-deliverable/SKILL.md

@@ -0,0 +1,60 @@
+---
+name: skill-deliverable
+description: 系统级。交付物自动上报引擎:自主解析agentId+sessionToken→调云函数入库。每个容器只报自己的活。
+schema_version: 0.0.1
+level: system
+category: reporting
+depends_on:
+  - skill-core-guide
+platforms:
+  - hermes-agent
+  - fmode-agent
+tags:
+  - hermes-agent
+  - fmode-agent
+  - deliverable
+  - reporting
+---
+
+# skill-deliverable — 交付物自动上报
+
+## 何时触发
+
+当每次任务完成后有交付物产出(文件/URL/代码/Git提交/部署),自动触发上报。
+
+## 核心流程
+
+1. **解析身份** — resolveAgentId() → FMODE_AGENT_ID > AGENT_ID > config > hostname
+2. **解析凭据** — resolveSessionToken() → env > user.json > config
+3. **调云函数** — POST /api/functions {token, id:"lfYlgU7SkK", params:{action:"report", agentId, ...}}
+4. **云函数处理** — 鉴权(owner) → 自动注册agent(首次) → 入库AgentDeliverable
+
+## 能力边界
+
+- ✅ 自动解析agentId+sessionToken(高兼容链)
+- ✅ 上报交付物(标题/摘要/项目/产出物/标签)
+- ✅ 查询本容器上报记录
+- ✅ 首次上报自动注册FmodeAgent记录
+- ❌ 不处理跨容器上报(每容器只报自己的活)
+- ❌ 不处理超级管理员角色检查(已移除)
+
+## CLI
+
+```bash
+skill-deliverable check
+skill-deliverable report --title "..." --summary "..."
+skill-deliverable list
+```
+
+## SDK
+
+```javascript
+import { report } from 'skill-deliverable';
+const r = await report({ agentId, title, summary, artifacts, tags });
+```
+
+## 配置
+
+- FMODE_SESSION_TOKEN(env或~/.fmode/config/user.json)
+- FMODE_AGENT_ID 或 AGENT_ID(env或~/.fmode/config.json)
+- 云函数 lfYlgU7SkK(v2版本需部署)