notes: 加一键转飞书文档 (sidecar markdown-to-feishu)
deploy notes / build-and-deploy (push) Failing after 2m2s

- backend: POST /api/recordings/:id/feishu → 拼 markdown (总结在最上 + 附件链接到转录/录音 + 转写全文) → 写 /data/feishu-tmp/<id>/ → HTTP POST 到 feishu sidecar
- 复用:已有 feishu_doc_id 时 --update 同一个 doc,前端按钮文案变「↻ 重新生成」
- schema 加 feishu_doc_id + feishu_url 两列(ALTER TABLE 兼容旧 db)
- LLM prompt 改:行动项用 markdown checkbox `- [ ] 谁·做什么·何时`
- sidecar apps/notes/feishu: node:20 + python3 + python3-markdown + @larksuite/cli + COPY 自己的 markdown-to-feishu script + FastAPI /convert
- k8s: deployment 加 feishu container 共享 PVC;lark-cli-creds Secret 挂 /root/.lark-cli/config.json
- CI: 主 image --no-cache(cube 规矩),sidecar 保留 layer cache(chromium-free,但 apt/npm 也大)
- 前端: content 头部加「📤 一键转飞书文档」按钮;已转过显示飞书链接 + 按钮变重生成
This commit is contained in:
Fam Zheng
2026-05-17 22:16:13 +01:00
parent 3a34fbdfd8
commit 68671784f6
8 changed files with 1327 additions and 11 deletions
+73
View File
@@ -79,6 +79,24 @@
<button v-if="selected.status === 'failed'" class="retry-btn" @click="retry"> 重试</button>
<button class="danger-btn" @click="remove">删除</button>
</div>
<div v-if="selected.status === 'done'" class="feishu-row">
<a
v-if="selected.feishu_url"
:href="selected.feishu_url"
target="_blank"
rel="noopener"
class="feishu-link"
>📄 飞书文档 · {{ selected.feishu_url.replace(/^https?:\/\//, '').slice(0, 40) }}</a>
<button
class="feishu-btn"
:disabled="feishuPushing"
@click="pushFeishu"
>
{{ feishuPushing ? '⏳ 推送中…'
: selected.feishu_url ? '↻ 重新生成' : '📤 一键转飞书文档' }}
</button>
<p v-if="feishuErr" class="feishu-err">{{ feishuErr }}</p>
</div>
</header>
<audio :src="audioUrl(selected.id)" controls class="audio" />
@@ -114,6 +132,7 @@ import {
uploadRecording,
deleteRecording,
retryRecording,
convertFeishu,
audioUrl as audioUrlFn,
getPass,
setPass,
@@ -130,6 +149,8 @@ const selected = ref(null)
const selectedId = ref(null)
const uploading = ref(false)
const uploadErr = ref('')
const feishuPushing = ref(false)
const feishuErr = ref('')
let pollTimer = null
// 浏览器内录音(iOS 没法选录音机 App 文件,直接 web record 更顺)
@@ -318,6 +339,23 @@ async function retry() {
} catch (e) { alert(e.message) }
}
async function pushFeishu() {
if (feishuPushing.value) return
feishuPushing.value = true
feishuErr.value = ''
try {
const r = await convertFeishu(selectedId.value)
if (selected.value) {
selected.value.feishu_doc_id = r.doc_id
selected.value.feishu_url = r.url
}
} catch (e) {
feishuErr.value = e.message || String(e)
} finally {
feishuPushing.value = false
}
}
function audioUrl(id) { return audioUrlFn(id) }
function statusLabel(s) {
@@ -580,6 +618,41 @@ input, textarea { font-family: inherit; background: transparent; border: none; c
padding: 3px 10px;
border-radius: 4px;
}
.feishu-row {
margin-top: 12px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.feishu-link {
color: var(--accent-cyan);
background: rgba(6, 182, 212, 0.1);
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
text-decoration: none;
}
.feishu-link:hover { background: rgba(6, 182, 212, 0.2); }
.feishu-btn {
background: var(--accent-strong);
color: #fff;
padding: 6px 14px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
.feishu-btn:hover:not(:disabled) { background: var(--accent); }
.feishu-err {
width: 100%;
margin: 0;
color: var(--accent-red);
background: rgba(239,68,68,0.08);
padding: 6px 10px;
border-radius: 4px;
font-size: 12px;
}
.retry-btn { background: rgba(124, 92, 191, 0.15); color: var(--accent); }
.retry-btn:hover { background: rgba(124, 92, 191, 0.3); }
.danger-btn { background: rgba(239, 68, 68, 0.1); color: var(--accent-red); }
+3
View File
@@ -35,6 +35,9 @@ export function listRecordings() { return jreq('/api/recordings') }
export function getRecording(id) { return jreq('/api/recordings/' + id) }
export function deleteRecording(id) { return jreq('/api/recordings/' + id, { method: 'DELETE' }) }
export function retryRecording(id) { return jreq('/api/recordings/' + id + '/retry', { method: 'POST' }) }
export function convertFeishu(id) {
return jreq('/api/recordings/' + id + '/feishu', { method: 'POST' })
}
export function uploadRecording(title, file) {
const fd = new FormData()