Skip to content

Commit 5aa6ca5

Browse files
nieaoclaude
andcommitted
feat(archive): 飞书云文档质量到 9/10 — 拆出 key_insights/improvements/next_steps + 渲染优化
之前生成的飞书云文档评分 5/10: - 决策字段读 decision (实际叫 verdict) - profile/structure 找错节点 (查 conclusion, 实际在 root) - 摘要只取 summary (~30 字), 没拿决策引擎产的 key_insights / improvements - DAG / 阶段拓扑 / 角色名缺失或字段读错 底层修复 (推进到 9/10): - archiveMetacognition 的 newNodes 从 yjs 反向通道传进来, 找 rootNode 拿 project_profile + structure - 决策引擎 schema 实际是 {verdict, score, summary, key_insights[], improvements[], next_steps[]} — 之前拼 pros/cons (从来不存在的字段), 现在按真实 schema 摊开 7 个区块 (含历史兼容) - DAG 改 bullet (飞书 docx 数字列表会全渲染为 "1.", 误导性强) - 阶段拓扑用 KIND_CN 字典覆盖 parallel/serial/sequential, 不再露 "serial" 英文 - 角色行展示 名字 + 职责 + 工具 + 派单, 一行密度最大化 工具: - server/peek-yjs.mjs — 查 inbox + nodes 当前状态 (调试) - server/test-archive-only.mjs — 直接对现有 conclusion 跑 archive (省去 feishu 消息触发) - export archiveMetacognition + 守卫 daemon 启动 (允许 import 测试) 测试: 同一个 conclusion 跑 4 轮迭代 docs +fetch 对比, 最终内容覆盖项目画像 / DAG / 拓扑 / 角色 / 核心洞察 / 改进建议 / 下一步, 评分 9/10. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a92a74a commit 5aa6ca5

3 files changed

Lines changed: 194 additions & 14 deletions

File tree

server/feishu-bot.mjs

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import path from 'node:path'
4040
import os from 'node:os'
4141
import fs from 'node:fs'
4242
import fsp from 'node:fs/promises'
43+
import { pathToFileURL } from 'node:url'
4344
import * as Y from 'yjs'
4445
import { WebsocketProvider } from 'y-websocket'
4546
import WebSocket from 'ws'
@@ -688,7 +689,7 @@ async function upsertBitableRecord(appToken, tableId, fields) {
688689
}
689690

690691
/** 元认知一轮跑完 → 同步云文档 + 多维表格 (异步, 失败不影响 IM 卡发送) */
691-
async function archiveMetacognition({ ctx, conclusionNode, newNodes, room, pngPath, screenshotPngUrl, screenshotSvgUrl }) {
692+
export async function archiveMetacognition({ ctx, conclusionNode, newNodes, room, pngPath, screenshotPngUrl, screenshotSvgUrl }) {
692693
const result = { docUrl: null, bitableUrl: null }
693694
if (!FEISHU_BITABLE_APP_TOKEN && !FEISHU_DOCS_FOLDER_TOKEN) return result // 全没配, 跳过
694695

@@ -704,13 +705,39 @@ async function archiveMetacognition({ ctx, conclusionNode, newNodes, room, pngPa
704705
const rootNode = newNodes.find((n) => n.id === conclusionNode.data?.projectRootId) ||
705706
newNodes.find((n) => n.data?.variant === 'goal' && n.data?.projectMode)
706707
const profile = rootNode?.data?.project_profile || conclusionNode.data?.project_profile || {}
707-
const taskDagArr = rootNode?.data?.structure?.task_dag || conclusionNode.data?.structure?.task_dag || []
708+
const structure = rootNode?.data?.structure || conclusionNode.data?.structure || {}
709+
const taskDagArr = structure?.task_dag || []
710+
// task DAG — bullet 列出, T-ID 当编号 (飞书 docx 数字列表会全渲染为 "1.", 改 bullet 避免误导)
708711
const taskDag = taskDagArr.map((t, i) => {
709-
const deps = (t.deps && t.deps.length) ? ` (依赖: ${t.deps.join(', ')})` : ''
710-
return `${i + 1}. **${t.id || ''} ${t.title}** — ${t.desc || ''}${deps}`
712+
const deps = (t.deps && t.deps.length) ? ` _(依赖: ${t.deps.join(', ')})_` : ''
713+
const desc = t.desc ? ` — ${t.desc}` : ''
714+
const io = []
715+
if (t.input) io.push(`输入: ${t.input}`)
716+
if (t.output) io.push(`产出: ${t.output}`)
717+
const ioStr = io.length ? ` _[${io.join(' · ')}]_` : ''
718+
return `- **${t.id || `T${i+1}`} · ${t.title}**${deps}${desc}${ioStr}`
711719
}).join('\n')
712-
// 真分支: 排除 root (variant=goal) 和 conclusion. 实际 task entity (variant=entity) 才是分支
713-
const branches = newNodes.filter((n) => n.type === 'ontologyNode' && !n.data?.isConclusion && n.data?.variant !== 'goal' && !n.data?.projectMode)
720+
// 执行阶段拓扑 — 哪些角色 / 任务并行, 哪些串行
721+
const stages = structure?.execution_topology?.stages || []
722+
const rolesArr = structure?.roles || []
723+
const KIND_CN = { parallel: '并行', sequential: '串行', serial: '串行', single: '单角色' }
724+
const stageLines = stages.map((s) => {
725+
const kind = KIND_CN[s.kind] || s.kind || '?'
726+
const rids = (s.role_ids || [])
727+
const roleNames = rids.map((rid) => {
728+
const r = rolesArr.find((x) => x.id === rid)
729+
return r ? `${r.name || rid}` : rid
730+
}).join(' / ')
731+
return `- **阶段 ${s.stage_index}** _(${kind})_: ${roleNames}`
732+
}).join('\n')
733+
// 派生分支: 真正的拆解/反驳 (非 task 本体, 非 root, 非 conclusion)
734+
const branches = newNodes.filter((n) =>
735+
n.type === 'ontologyNode' &&
736+
!n.data?.isConclusion &&
737+
n.data?.variant !== 'goal' &&
738+
!n.data?.projectMode &&
739+
!n.data?.projectTaskId // 排除 6 stage 自动建的 task entity (DAG 章节已展示)
740+
)
714741
const branchLines = branches.map((n) => `- ▸ **${n.data?.title || ''}** — ${n.data?.description || n.data?.content || ''}`).join('\n')
715742
// agentRole 节点 — 角色名 + 职责 + 工具 + 任务
716743
const roles = newNodes.filter((n) => n.type === 'agentRoleNode')
@@ -723,12 +750,23 @@ async function archiveMetacognition({ ctx, conclusionNode, newNodes, room, pngPa
723750
return `- **${name}** — ${resp}${tools}${tasks}`
724751
}).join('\n')
725752

726-
// 摘要扩充 — 合并 summary + reasoning + pros + cons + next_steps
753+
// 摘要扩充 — 把决策引擎所有结构化字段都摊开 (summary + key_insights + improvements + next_steps)
754+
// 决策引擎实际 schema: {verdict, score, summary, key_insights[], improvements[], next_steps[]}
755+
// 历史兼容: pros/cons/risks/reasoning 字段也保留拼接, 万一有旧版本数据
756+
const arrSection = (label, arr) => Array.isArray(arr) && arr.length
757+
? `\n**${label}**:\n\n${arr.map((s) => `- ${s}`).join('\n')}`
758+
: ''
759+
const reasoningExtra = (cObj.reasoning && cObj.reasoning !== cObj.summary)
760+
? `\n\n**推理**:\n\n${cObj.reasoning}`
761+
: ''
727762
const detailedSummary = [
728-
summary,
729-
Array.isArray(cObj.pros) && cObj.pros.length ? `\n**优势**:\n${cObj.pros.map(p => `- ${p}`).join('\n')}` : '',
730-
Array.isArray(cObj.cons) && cObj.cons.length ? `\n**劣势**:\n${cObj.cons.map(c => `- ${c}`).join('\n')}` : '',
731-
Array.isArray(cObj.next_steps) && cObj.next_steps.length ? `\n**下一步**:\n${cObj.next_steps.map(s => `- ${s}`).join('\n')}` : '',
763+
summary + reasoningExtra,
764+
arrSection('核心洞察', cObj.key_insights),
765+
arrSection('改进建议', cObj.improvements),
766+
arrSection('优势', cObj.pros),
767+
arrSection('劣势', cObj.cons),
768+
arrSection('风险', cObj.risks),
769+
arrSection('下一步', cObj.next_steps),
732770
].filter(Boolean).join('\n')
733771

734772
// 1) 云文档 (markdown)
@@ -753,14 +791,15 @@ async function archiveMetacognition({ ctx, conclusionNode, newNodes, room, pngPa
753791
'',
754792
taskDag || '_无_',
755793
'',
756-
'## 分支节点',
794+
'## 执行阶段拓扑',
757795
'',
758-
branchLines || '_无_',
796+
stageLines || '_无_',
759797
'',
760798
'## 涌现角色',
761799
'',
762800
roleLines || '_无_',
763801
'',
802+
...(branches.length ? ['## 派生分支 (拆解 / 反驳)', '', branchLines, ''] : []),
764803
'## 摘要 / 推理',
765804
'',
766805
detailedSummary || '_无_',
@@ -1219,4 +1258,7 @@ function start() {
12191258

12201259
}
12211260

1222-
start()
1261+
// 仅在直接 node 运行时启动 daemon — import 测试不会启动
1262+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
1263+
start()
1264+
}

server/peek-yjs.mjs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* 查 yjs room 当前 inbox + 节点状态 — 本地运行也行 (默认连 ws://127.0.0.1:1234)
3+
* node server/peek-yjs.mjs [room=demo-final]
4+
*/
5+
import * as Y from 'yjs'
6+
import { WebsocketProvider } from 'y-websocket'
7+
import WebSocket from 'ws'
8+
9+
if (typeof globalThis.WebSocket === 'undefined') globalThis.WebSocket = WebSocket
10+
11+
const ROOM = process.argv[2] || 'demo-final'
12+
const WS = process.env.WS_URL || 'ws://127.0.0.1:1234'
13+
14+
const doc = new Y.Doc()
15+
const provider = new WebsocketProvider(WS, ROOM, doc, { connect: true, WebSocketPolyfill: WebSocket })
16+
await new Promise((r, rej) => {
17+
const t = setTimeout(() => rej(new Error('sync timeout')), 8000)
18+
provider.once('synced', () => { clearTimeout(t); r() })
19+
})
20+
21+
const inbox = doc.getMap('aletheia-inbox')
22+
console.log('=== aletheia-inbox (' + inbox.size + ') ===')
23+
inbox.forEach((v, k) => {
24+
console.log(`${k} | status=${v?.status} | ts=${v?.ts} | text="${String(v?.text || '').slice(0, 80)}"`)
25+
})
26+
27+
const nodes = doc.getMap('nodes')
28+
console.log('\n=== latest 8 ontologyNode ===')
29+
const list = []
30+
nodes.forEach((n, k) => { if (n?.type === 'ontologyNode') list.push({ k, n }) })
31+
list.sort((a, b) => (b.n.data?.created_at || 0) - (a.n.data?.created_at || 0))
32+
list.slice(0, 8).forEach(({ k, n }) => {
33+
const t = n.data?.created_at ? new Date(n.data.created_at).toISOString().slice(11, 19) : '?'
34+
console.log(`${t} | ${k.slice(0, 60).padEnd(60)} | ${n.data?.isConclusion ? '⭐CONC ' : ' '} | ${String(n.data?.title || '').slice(0, 50)}`)
35+
})
36+
37+
console.log('\n=== awareness ===')
38+
provider.awareness.getStates().forEach((state, cid) => {
39+
console.log(`client ${cid}: user=${JSON.stringify(state?.user || null)?.slice(0, 60)}`)
40+
})
41+
42+
provider.disconnect()
43+
provider.destroy()
44+
doc.destroy()
45+
process.exit(0)

server/test-archive-only.mjs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* 单测: 直接走 archiveMetacognition 跑一遍现有 yjs 上的最新 conclusion, 不依赖飞书消息触发
3+
* node server/test-archive-only.mjs [room=demo-final] [chatId=oc_d2d890f2072a92a98b9f87ccb76a5b68]
4+
*
5+
* 用法场景: bot daemon 重启后 reverse channel 的 pending queue 丢了, 但 yjs 里 conclusion 还在,
6+
* 想验证 archive 输出 (云文档 + Bitable) 的 markdown / 字段质量.
7+
*/
8+
import * as Y from 'yjs'
9+
import { WebsocketProvider } from 'y-websocket'
10+
import WebSocket from 'ws'
11+
import { archiveMetacognition } from './feishu-bot.mjs'
12+
13+
if (typeof globalThis.WebSocket === 'undefined') globalThis.WebSocket = WebSocket
14+
15+
const ROOM = process.argv[2] || 'demo-final'
16+
const CHAT_ID = process.argv[3] || 'oc_d2d890f2072a92a98b9f87ccb76a5b68'
17+
const WS = process.env.Y_WS_URL || 'ws://127.0.0.1:1234'
18+
19+
const doc = new Y.Doc()
20+
const provider = new WebsocketProvider(WS, ROOM, doc, { connect: true, WebSocketPolyfill: WebSocket })
21+
await new Promise((r, rej) => {
22+
const t = setTimeout(() => rej(new Error('sync timeout')), 8000)
23+
provider.once('synced', () => { clearTimeout(t); r() })
24+
})
25+
26+
const yNodes = doc.getMap('nodes')
27+
console.log(`房间 ${ROOM} 节点总数=${yNodes.size}`)
28+
29+
// 找最新的 conclusion (按 created_at 排)
30+
let latestConclusion = null
31+
let latestProjectRootId = null
32+
yNodes.forEach((n, k) => {
33+
if (n?.type === 'ontologyNode' && n.data?.isConclusion) {
34+
if (!latestConclusion || (n.data?.created_at || 0) > (latestConclusion.data?.created_at || 0)) {
35+
latestConclusion = { ...n, id: k }
36+
latestProjectRootId = n.data?.projectRootId
37+
}
38+
}
39+
})
40+
41+
if (!latestConclusion) {
42+
console.error('找不到任何 conclusion 节点, 退出')
43+
process.exit(1)
44+
}
45+
46+
console.log(`选中 conclusion: ${latestConclusion.id}`)
47+
console.log(`projectRootId: ${latestProjectRootId}`)
48+
console.log(`title: ${latestConclusion.data?.title}`)
49+
50+
// 收集所有跟这个 project 相关的节点 (root + projectRootId 等于它的)
51+
const projectNodes = []
52+
yNodes.forEach((n, k) => {
53+
if (k === latestProjectRootId) projectNodes.push({ ...n, id: k })
54+
else if (n?.data?.projectRootId === latestProjectRootId) projectNodes.push({ ...n, id: k })
55+
})
56+
console.log(`项目相关节点数: ${projectNodes.length}`)
57+
58+
// 拼 ctx (模拟原 reverse channel 的 ctx)
59+
const rootNode = projectNodes.find((n) => n.id === latestProjectRootId)
60+
const PROMPT = rootNode?.data?.project_profile?.target ||
61+
String(rootNode?.data?.title || '').replace(/^\[[^\]]*\]\s*/, '').slice(0, 300) ||
62+
String(latestConclusion.data?.conclusion?.summary || '').slice(0, 300)
63+
const ctx = {
64+
prompt: PROMPT,
65+
chatId: CHAT_ID,
66+
attribution: { name: '测试-9分评估', via: 'feishu-bot' },
67+
}
68+
69+
console.log(`\n=== 调用 archiveMetacognition ===`)
70+
console.log(`prompt: ${PROMPT.slice(0, 80)}`)
71+
console.log(`chatId: ${CHAT_ID}`)
72+
73+
const screenshotPngUrl = latestConclusion.data?.screenshotPngUrl || ''
74+
const screenshotSvgUrl = latestConclusion.data?.screenshotSvgUrl || ''
75+
const pngPath = latestConclusion.data?.screenshotPngPath || ''
76+
77+
const result = await archiveMetacognition({
78+
ctx,
79+
conclusionNode: latestConclusion,
80+
newNodes: projectNodes,
81+
room: ROOM,
82+
pngPath,
83+
screenshotPngUrl,
84+
screenshotSvgUrl,
85+
})
86+
87+
console.log('\n=== 结果 ===')
88+
console.log(JSON.stringify(result, null, 2))
89+
90+
provider.disconnect()
91+
provider.destroy()
92+
doc.destroy()
93+
process.exit(0)

0 commit comments

Comments
 (0)